class Beaker::Openstack

Beaker support for OpenStack This code is EXPERIMENTAL! Please file any issues/concerns at github.com/puppetlabs/beaker/issues

Constants

SLEEPWAIT

Public Class Methods

new(openstack_hosts, options) click to toggle source

Create a new instance of the OpenStack hypervisor object @param [<Host>] openstack_hosts The array of OpenStack hosts to provision @param [Hash{Symbol=>String}] options The options hash containing configuration values @option options [String] :openstack_api_key The key to access the OpenStack instance with (required) @option options [String] :openstack_username The username to access the OpenStack instance with (required) @option options [String] :openstack_auth_url The URL to access the OpenStack instance with (required) @option options [String] :openstack_tenant The tenant to access the OpenStack instance with (either this or openstack_project_name is required) @option options [String] :openstack_project_name The project name to access the OpenStack instance with (either this or openstack_tenant is required) @option options [String] :openstack_project_id The project id to access the OpenStack instance with (alternative to openstack_project_name) @option options [String] :openstack_user_domain The user domain name to access the OpenStack instance with @option options [String] :openstack_user_domain_id The user domain id to access the OpenStack instance with (alternative to openstack_user_domain) @option options [String] :openstack_project_domain The project domain to access the OpenStack instance with @option options [String] :openstack_project_domain_id The project domain id to access the OpenStack instance with (alternative to openstack_project_domain) @option options [String] :openstack_region The region that each OpenStack instance should be provisioned on (optional) @option options [String] :openstack_network The network that each OpenStack instance should be contacted through (required) @option options [Bool] :openstack_floating_ip Whether a floating IP should be allocated (required) @option options [String] :openstack_keyname The name of an existing key pair that should be auto-loaded onto each @option options [Hash] :security_group An array of security groups to associate with the instance

OpenStack instance (optional)

@option options [String] :jenkins_build_url Added as metadata to each OpenStack instance @option options [String] :department Added as metadata to each OpenStack instance @option options [String] :project Added as metadata to each OpenStack instance @option options [Integer] :timeout The amount of time to attempt execution before quiting and exiting with failure

# File lib/beaker/hypervisor/openstack.rb, line 32
def initialize(openstack_hosts, options)
  require 'fog/openstack'
  @options = options
  @logger = options[:logger]
  @hosts = openstack_hosts
  @vms = []

  raise 'You must specify an Openstack API key (:openstack_api_key) for OpenStack instances!' unless @options[:openstack_api_key]
  raise 'You must specify an Openstack username (:openstack_username) for OpenStack instances!' unless @options[:openstack_username]
  raise 'You must specify an Openstack auth URL (:openstack_auth_url) for OpenStack instances!' unless @options[:openstack_auth_url]
  raise 'You must specify an Openstack network (:openstack_network) for OpenStack instances!' unless @options[:openstack_network]
  raise 'You must specify whether a floating IP (:openstack_floating_ip) should be used for OpenStack instances!' unless !@options[:openstack_floating_ip].nil?

  is_v3 = @options[:openstack_auth_url].include?('/v3/')
  raise 'You must specify an Openstack tenant (:openstack_tenant) for OpenStack instances!' if !is_v3 and !@options[:openstack_tenant]
  raise 'You must specify an Openstack project name (:openstack_project_name) or Openstack project id (:openstack_project_id) for OpenStack instances!' if is_v3 and (!@options[:openstack_project_name] and !@options[:openstack_project_id])
  raise 'You must specify either Openstack project name (:openstack_project_name) or Openstack project id (:openstack_project_id) not both!' if is_v3 and (@options[:openstack_project_name] and @options[:openstack_project_id])
  raise 'You may specify either Openstack user domain (:openstack_user_domain) or Openstack user domain id (:openstack_user_domain_id) not both!' if is_v3 and (@options[:openstack_user_domain] and @options[:openstack_user_domain_id])
  raise 'You may specify either Openstack project domain (:openstack_project_domain) or Openstack project domain id (:openstack_project_domain_id) not both!' if is_v3 and (@options[:openstack_project_domain] and @options[:openstack_project_domain_id])
  raise 'Invalid option specified: v3 API expects :openstack_project_name or :openstack_project_id, not :openstack_tenant for OpenStack instances!' if is_v3 and @options[:openstack_tenant]
  raise 'Invalid option specified: v2 API expects :openstack_tenant, not :openstack_project_name or :openstack_project_id for OpenStack instances!' if !is_v3 and (@options[:openstack_project_name] or @options[:openstack_project_id])
  # Ensure that _id and non _id params are not mixed (due to bug in fog-openstack)
  raise 'You must not mix _id values non _id (name) values. Please use the same type for (:openstack_project_), (:openstack_user_domain) and (:openstack_project_domain)!' if is_v3 and (@options[:openstack_project_name] or @options[:openstack_user_domain] or @options[:openstack_project_domain]) and (@options[:openstack_project_id] or @options[:openstack_user_domain_id] or @options[:openstack_project_domain_id])

  # Keystone version 3 changed the parameter names
  if !is_v3
    extra_credentials = {:openstack_tenant => @options[:openstack_tenant]}
  else
    if @options[:openstack_project_id]
      extra_credentials = {:openstack_project_id => @options[:openstack_project_id]}
    else
      extra_credentials = {:openstack_project_name => @options[:openstack_project_name]}
    end
  end

  # Common keystone authentication credentials
  @credentials = {
    :provider           => :openstack,
    :openstack_auth_url => @options[:openstack_auth_url],
    :openstack_api_key  => @options[:openstack_api_key],
    :openstack_username => @options[:openstack_username],
    :openstack_tenant   => @options[:openstack_tenant],
    :openstack_region   => @options[:openstack_region],
  }.merge(extra_credentials)

  # Keystone version 3 requires users and projects to be scoped
  if is_v3
    if @options[:openstack_user_domain_id]
      @credentials[:openstack_user_domain_id] = @options[:openstack_user_domain_id]
    else
      @credentials[:openstack_user_domain]    = @options[:openstack_user_domain] || 'Default'
    end
    if @options[:openstack_project_domain_id]
      @credentials[:openstack_project_domain_id] = @options[:openstack_project_domain_id]
    else
      @credentials[:openstack_project_domain]    = @options[:openstack_project_domain] || 'Default'
    end        
  end

  @compute_client ||= Fog::Compute.new(@credentials)

  if not @compute_client
    raise "Unable to create OpenStack Compute instance (api key: #{@options[:openstack_api_key]}, username: #{@options[:openstack_username]}, auth_url: #{@options[:openstack_auth_url]}, tenant: #{@options[:openstack_tenant]}, project_name: #{@options[:openstack_project_name]})"
  end

  @network_client ||= Fog::Network.new(@credentials)

  if not @network_client
    raise "Unable to create OpenStack Network instance (api key: #{@options[:openstack_api_key]}, username: #{@options[:openstack_username]}, auth_url: #{@options[:openstack_auth_url]}, tenant: #{@options[:openstack_tenant]}, project_name: #{@options[:openstack_project_name]})"
  end

  # Validate openstack_volume_support setting value, reset to boolean if passed via ENV value string
  @options[:openstack_volume_support] = true  if @options[:openstack_volume_support].to_s.match(/\btrue\b/i)
  @options[:openstack_volume_support] = false if @options[:openstack_volume_support].to_s.match(/\bfalse\b/i)
  [true,false].include? @options[:openstack_volume_support] or raise "Invalid openstack_volume_support setting, current: @options[:openstack_volume_support]"

end

Public Instance Methods

cleanup() click to toggle source

Destroy any OpenStack instances

# File lib/beaker/hypervisor/openstack.rb, line 333
def cleanup
  @logger.notify "Cleaning up OpenStack"
  @vms.each do |vm|
    cleanup_storage(vm) if @options[:openstack_volume_support]
    @logger.debug "Release floating IPs for OpenStack host #{vm.name}"
    floating_ips = vm.all_addresses # fetch and release its floating IPs
    floating_ips.each do |address|
      @compute_client.disassociate_address(vm.id, address['ip'])
      @compute_client.release_address(address['id'])
    end
    @logger.debug "Destroying OpenStack host #{vm.name}"
    vm.destroy
    if @options[:openstack_keyname].nil?
      @logger.debug "Deleting random keypair"
      @compute_client.delete_key_pair vm.key_name
    end
  end
end
cleanup_storage(vm) click to toggle source

Detach and delete guest volumes @param vm [Fog::Compute::OpenStack::Server] the server to detach from

# File lib/beaker/hypervisor/openstack.rb, line 226
def cleanup_storage vm
  vm.volumes.each do |vol|
    @logger.debug "Deleting volume #{vol.name} for OpenStack host #{vm.name}"
    vm.detach_volume(vol.id)
    vol.wait_for { ready? }
    vol.destroy
  end
end
create_or_associate_keypair(host, keyname) click to toggle source

Get key_name from options or generate a new rsa key and add it to OpenStack keypairs

@param [Host] host The OpenStack host to provision @api private

# File lib/beaker/hypervisor/openstack.rb, line 380
def create_or_associate_keypair(host, keyname)
  if @options[:openstack_keyname]
    @logger.debug "Adding optional key_name #{@options[:openstack_keyname]} to #{host.name} (#{host[:vmhostname]})"
    keyname = @options[:openstack_keyname]
  else
    @logger.debug "Generate a new rsa key"

    # There is apparently an error that can occur when generating RSA keys, probably
    # due to some timing issue, probably similar to the issue described here:
    # https://github.com/negativecode/vines/issues/34
    # In order to mitigate this error, we will simply try again up to three times, and
    # then fail if we continue to error out.
    begin
      retries ||= 0
      key = OpenSSL::PKey::RSA.new 2048
    rescue OpenSSL::PKey::RSAError => e
      retries += 1
      if retries > 2
        @logger.notify "error generating RSA key #{retries} times, exiting"
        raise e
      end
      retry
    end

    type = key.ssh_type
    data = [ key.to_blob ].pack('m0')
    @logger.debug "Creating Openstack keypair '#{keyname}' for public key '#{type} #{data}'"
    @compute_client.create_key_pair keyname, "#{type} #{data}"
    host['ssh'][:key_data] = [ key.to_pem ]
  end

  host[:keyname] = keyname
end
enable_root(host) click to toggle source

enable root on a single host (the current one presumably) but only if the username isn't 'root'

# File lib/beaker/hypervisor/openstack.rb, line 366
def enable_root(host)
  if host['user'] != 'root'
    copy_ssh_to_root(host, @options)
    enable_root_login(host, @options)
    host['user'] = 'root'
    host.close
  end
end
enable_root_on_hosts() click to toggle source

Enables root access for a host when username is not root This method ripped from the aws_sdk implementation and is probably wrong because it iterates on a collection when there's no guarantee the collection has all been brought up in openstack yet and will thus explode @return [void] @api private

# File lib/beaker/hypervisor/openstack.rb, line 358
def enable_root_on_hosts
  @hosts.each do |host|
    enable_root(host)
  end
end
flavor(f) click to toggle source

Provided a flavor name return the OpenStack id for that flavor @param [String] f The flavor name @return [String] Openstack id for provided flavor name

# File lib/beaker/hypervisor/openstack.rb, line 113
def flavor f
  @logger.debug "OpenStack: Looking up flavor '#{f}'"
  @compute_client.flavors.find { |x| x.name == f } || raise("Couldn't find flavor: #{f}")
end
get_floating_ip() click to toggle source

Get a floating IP address to associate with the instance, try to allocate a new one from the specified pool if none are available

TODO(GiedriusS): convert to use @network_client. This API will be turned off completely very soon.

# File lib/beaker/hypervisor/openstack.rb, line 240
def get_floating_ip
  begin
    @logger.debug "Creating IP"
    ip = @compute_client.addresses.create
  rescue Fog::OpenStack::Compute::NotFound
    # If there are no more floating IP addresses, allocate a
    # new one and try again.
    @compute_client.allocate_address(@options[:floating_ip_pool])
    ip = @compute_client.addresses.find { |ip| ip.instance_id.nil? }
  end
  ip
end
get_volume_api_version() click to toggle source

Get the API version

# File lib/beaker/hypervisor/openstack.rb, line 166
def get_volume_api_version
  case @volume_client
  when Fog::Volume::OpenStack::V1
    1
  else
    -1
  end
end
get_volumes(host) click to toggle source

Get a hash of volumes from the host

# File lib/beaker/hypervisor/openstack.rb, line 160
def get_volumes host
  return host['volumes'] if host['volumes']
  {}
end
image(i) click to toggle source

Provided an image name return the OpenStack id for that image @param [String] i The image name @return [String] Openstack id for provided image name

# File lib/beaker/hypervisor/openstack.rb, line 121
def image i
  @logger.debug "OpenStack: Looking up image '#{i}'"
  @compute_client.images.find { |x| x.name == i } || raise("Couldn't find image: #{i}")
end
network(n) click to toggle source

Provided a network name return the OpenStack id for that network @param [String] n The network name @return [String] Openstack id for provided network name

# File lib/beaker/hypervisor/openstack.rb, line 129
def network n
  @logger.debug "OpenStack: Looking up network '#{n}'"
  @network_client.networks.find { |x| x.name == n } || raise("Couldn't find network: #{n}")
end
provision() click to toggle source

Create new instances in OpenStack

# File lib/beaker/hypervisor/openstack.rb, line 254
def provision
  @logger.notify "Provisioning OpenStack"

  @hosts.each do |host|
    if @options[:openstack_floating_ip]
      ip = get_floating_ip
      hostname = ip.ip.gsub('.','-')
      host[:vmhostname] = hostname + '.rfc1918.puppetlabs.net'
    else
      hostname = ('a'..'z').to_a.shuffle[0, 10].join
      host[:vmhostname] = hostname
    end

    create_or_associate_keypair(host, hostname)
    @logger.debug "Provisioning #{host.name} (#{host[:vmhostname]})"
    options = {
      :flavor_ref => flavor(host[:flavor]).id,
      :image_ref  => image(host[:image]).id,
      :nics       => [ {'net_id' => network(@options[:openstack_network]).id } ],
      :name       => host[:vmhostname],
      :hostname   => host[:vmhostname],
      :user_data  => host[:user_data] || "#cloud-config\nmanage_etc_hosts: true\n",
      :key_name   => host[:keyname],
    }
    options[:security_groups] = security_groups(@options[:security_group]) unless @options[:security_group].nil?
    vm = @compute_client.servers.create(options)

    #wait for the new instance to start up
    try = 1
    attempts = @options[:timeout].to_i / SLEEPWAIT

    while try <= attempts
      begin
        vm.wait_for(5) { ready? }
        break
      rescue Fog::Errors::TimeoutError => e
        if try >= attempts
          @logger.debug "Failed to connect to new OpenStack instance #{host.name} (#{host[:vmhostname]})"
          raise e
        end
        @logger.debug "Timeout connecting to instance #{host.name} (#{host[:vmhostname]}), trying again..."
      end
      sleep SLEEPWAIT
      try += 1
    end

    if @options[:openstack_floating_ip]
      # Associate a public IP to the VM
      ip.server = vm
      host[:ip] = ip.ip
    else
      # Get the first address of the VM that was just created just like in the
      # OpenStack UI
      host[:ip] = vm.addresses.first[1][0]["addr"]
    end

    @logger.debug "OpenStack host #{host.name} (#{host[:vmhostname]}) assigned ip: #{host[:ip]}"

    #set metadata
    vm.metadata.update({:jenkins_build_url => @options[:jenkins_build_url].to_s,
                        :department        => @options[:department].to_s,
                        :project           => @options[:project].to_s })
    @vms << vm

    # Wait for the host to accept ssh logins
    host.wait_for_port(22)

    #enable root if user is not root
    enable_root(host)

    provision_storage(host, vm) if @options[:openstack_volume_support]
    @logger.notify "OpenStack Volume Support Disabled, can't provision volumes" if not @options[:openstack_volume_support]
  end

  hack_etc_hosts @hosts, @options

end
provision_storage(host, vm) click to toggle source

Create and attach dynamic volumes

Creates an array of volumes and attaches them to the current host. The host bus type is determined by the image type, so by default devices appear as /dev/vdb, /dev/vdc etc. Setting the glance properties hw_disk_bus=scsi, hw_scsi_model=virtio-scsi will present them as /dev/sdb, /dev/sdc (or 2:0:0:1, 2:0:0:2 in SCSI addresses)

@param host [Hash] thet current host defined in the nodeset @param vm [Fog::Compute::OpenStack::Server] the server to attach to

# File lib/beaker/hypervisor/openstack.rb, line 185
def provision_storage host, vm
  volumes = get_volumes(host)
  if !volumes.empty?
    # Lazily create the volume client if needed
    volume_client_create
    volumes.keys.each_with_index do |volume, index|
      @logger.debug "Creating volume #{volume} for OpenStack host #{host.name}"

      # The node defintion file defines volume sizes in MB (due to precedent
      # with the vagrant virtualbox implementation) however OpenStack requires
      # this translating into GB
      openstack_size = volumes[volume]['size'].to_i / 1000

      # Set up the volume creation arguments
      args = {
        :size        => openstack_size,
        :description => "Beaker volume: host=#{host.name} volume=#{volume}",
      }

      # Between version 1 and subsequent versions the API was updated to
      # rename 'display_name' to just 'name' for better consistency
      if get_volume_api_version == 1
        args[:display_name] = volume
      else
        args[:name] = volume
      end

      # Create the volume and wait for it to become available
      vol = @volume_client.volumes.create(**args)
      vol.wait_for { ready? }

      # Fog needs a device name to attach as, so invent one.  The guest
      # doesn't pay any attention to this
      device = "/dev/vd#{('b'.ord + index).chr}"
      vm.attach_volume(vol.id, device)
    end
  end
end
security_groups(sgs) click to toggle source

Provided an array of security groups return that array if all security groups are present @param [Array] sgs The array of security group names @return [Array] The array of security group names

# File lib/beaker/hypervisor/openstack.rb, line 138
def security_groups sgs
  for sg in sgs
    @logger.debug "Openstack: Looking up security group '#{sg}'"
    @compute_client.security_groups.find { |x| x.name == sg } || raise("Couldn't find security group: #{sg}")
    sgs
  end
end
volume_client_create() click to toggle source

Create a volume client on request @return [Fog::OpenStack::Volume] OpenStack volume client

# File lib/beaker/hypervisor/openstack.rb, line 148
def volume_client_create
  @volume_client ||= Fog::Volume.new(@credentials)
  unless @volume_client
    raise "Unable to create OpenStack Volume instance"\
      " (api_key: #{@options[:openstack_api_key]},"\
    " username: #{@options[:openstack_username]},"\
    " auth_url: #{@options[:openstack_auth_url]},"\
    " tenant: #{@options[:openstack_tenant]})"
  end
end