Class: Beaker::OpenStack

Inherits:
Hypervisor show all
Defined in:
lib/beaker/hypervisor/openstack.rb

Overview

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

Constant Summary collapse

SLEEPWAIT =
5

Constants inherited from Hypervisor

Hypervisor::CHARMAP

Constants included from HostPrebuiltSteps

HostPrebuiltSteps::APT_CFG, HostPrebuiltSteps::CUMULUS_PACKAGES, HostPrebuiltSteps::DEBIAN_PACKAGES, HostPrebuiltSteps::ETC_HOSTS_PATH, HostPrebuiltSteps::ETC_HOSTS_PATH_SOLARIS, HostPrebuiltSteps::FREEBSD_PACKAGES, HostPrebuiltSteps::IPS_PKG_REPO, HostPrebuiltSteps::NTPSERVER, HostPrebuiltSteps::OPENBSD_PACKAGES, HostPrebuiltSteps::PSWINDOWS_PACKAGES, HostPrebuiltSteps::ROOT_KEYS_SCRIPT, HostPrebuiltSteps::ROOT_KEYS_SYNC_CMD, HostPrebuiltSteps::ROOT_KEYS_SYNC_CMD_AIX, HostPrebuiltSteps::SLES10_PACKAGES, HostPrebuiltSteps::SLES_PACKAGES, HostPrebuiltSteps::SOLARIS10_PACKAGES, HostPrebuiltSteps::SOLARIS11_PACKAGES, HostPrebuiltSteps::TRIES, HostPrebuiltSteps::UNIX_PACKAGES, HostPrebuiltSteps::WINDOWS_PACKAGES

Instance Method Summary collapse

Methods inherited from Hypervisor

#configure, create, #generate_host_name, #proxy_package_manager, #validate

Methods included from HostPrebuiltSteps

#add_el_extras, #additive_hash_merge, #apt_get_update, #check_and_install_packages_if_needed, #construct_env, #copy_file_to_remote, #copy_ssh_to_root, #disable_iptables, #disable_se_linux, #disable_updates, #enable_root_login, #get_domain_name, #hack_etc_hosts, #install_one_of_packages, #package_proxy, #proxy_config, #set_env, #set_etc_hosts, #sync_root_keys, #timesync, #validate_host

Methods included from DSL::Patterns

#block_on

Constructor Details

#initialize(openstack_hosts, options) ⇒ OpenStack

Create a new instance of the OpenStack hypervisor object

Parameters:

  • openstack_hosts (<Host>)

    The array of OpenStack hosts to provision

  • options (Hash{Symbol=>String})

    The options hash containing configuration values

Options Hash (options):

  • :openstack_api_key (String)

    The key to access the OpenStack instance with (required)

  • :openstack_username (String)

    The username to access the OpenStack instance with (required)

  • :openstack_auth_url (String)

    The URL to access the OpenStack instance with (required)

  • :openstack_tenant (String)

    The tenant to access the OpenStack instance with (required)

  • :openstack_region (String)

    The region that each OpenStack instance should be provisioned on (optional)

  • :openstack_network (String)

    The network that each OpenStack instance should be contacted through (required)

  • :openstack_keyname (String)

    The name of an existing key pair that should be auto-loaded onto each

  • :security_group (Hash)

    An array of security groups to associate with the instance OpenStack instance (optional)

  • :jenkins_build_url (String)

    Added as metadata to each OpenStack instance

  • :department (String)

    Added as metadata to each OpenStack instance

  • :project (String)

    Added as metadata to each OpenStack instance

  • :timeout (Integer)

    The amount of time to attempt execution before quiting and exiting with failure



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/beaker/hypervisor/openstack.rb', line 25

def initialize(openstack_hosts, options)
  require 'fog'
  @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 tenant (:openstack_tenant) for OpenStack instances!' unless @options[:openstack_tenant]
  raise 'You must specify an Openstack network (:openstack_network) for OpenStack instances!' unless @options[:openstack_network]

  optionhash = {}
  optionhash[:provider]           = :openstack
  optionhash[:openstack_api_key]  = @options[:openstack_api_key]
  optionhash[:openstack_username] = @options[:openstack_username]
  optionhash[:openstack_auth_url] = @options[:openstack_auth_url]
  optionhash[:openstack_tenant]   = @options[:openstack_tenant]
  optionhash[:openstack_region]   = @options[:openstack_region] if @options[:openstack_region]

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

  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]})"
  end

  networkoptionhash = {}
  networkoptionhash[:provider]           = :openstack
  networkoptionhash[:openstack_api_key]  = @options[:openstack_api_key]
  networkoptionhash[:openstack_username] = @options[:openstack_username]
  networkoptionhash[:openstack_auth_url] = @options[:openstack_auth_url]
  networkoptionhash[:openstack_tenant]   = @options[:openstack_tenant]
  networkoptionhash[:openstack_region]   = @options[:openstack_region] if @options[:openstack_region]

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

  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]})"
  end

end

Instance Method Details

#cleanupObject

Destroy any OpenStack instances



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/beaker/hypervisor/openstack.rb', line 262

def cleanup
  @logger.notify "Cleaning up OpenStack"
  @vms.each do |vm|
    cleanup_storage(vm)
    @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) ⇒ Object

Detach and delete guest volumes

Parameters:

  • vm (Fog::Compute::OpenStack::Server)

    the server to detach from



165
166
167
168
169
170
171
172
# File 'lib/beaker/hypervisor/openstack.rb', line 165

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

#enable_root(host) ⇒ Object

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



295
296
297
298
299
300
301
302
# File 'lib/beaker/hypervisor/openstack.rb', line 295

def enable_root(host)
  if host['user'] != 'root'
    copy_ssh_to_root(host, @options)
    (host, @options)
    host['user'] = 'root'
    host.close
  end
end

#enable_root_on_hostsvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

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



287
288
289
290
291
# File 'lib/beaker/hypervisor/openstack.rb', line 287

def enable_root_on_hosts
  @hosts.each do |host|
    enable_root(host)
  end
end

#flavor(f) ⇒ String

Provided a flavor name return the OpenStack id for that flavor

Parameters:

  • f (String)

    The flavor name

Returns:

  • (String)

    Openstack id for provided flavor name



71
72
73
74
# File 'lib/beaker/hypervisor/openstack.rb', line 71

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_ipObject

Get a floating IP address to associate with the instance, and allocate a new one if none are available



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/beaker/hypervisor/openstack.rb', line 176

def get_ip
  @logger.debug "Finding IP"
  ip = @compute_client.addresses.find { |ip| ip.instance_id.nil? }
  if ip.nil?
    begin
      @logger.debug "Creating IP"
      ip = @compute_client.addresses.create
    rescue Fog::Compute::OpenStack::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
  end
  raise 'Could not find or allocate an address' if not ip
  ip
end

#image(i) ⇒ String

Provided an image name return the OpenStack id for that image

Parameters:

  • i (String)

    The image name

Returns:

  • (String)

    Openstack id for provided image name



79
80
81
82
# File 'lib/beaker/hypervisor/openstack.rb', line 79

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

#key_name(host) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Parameters:

  • host (Host)

    The OpenStack host to provision

Returns:

  • (String)

    key_name



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/beaker/hypervisor/openstack.rb', line 310

def key_name(host)
  if @options[:openstack_keyname]
    @logger.debug "Adding optional key_name #{@options[:openstack_keyname]} to #{host.name} (#{host[:vmfqdn]})"
    @options[:openstack_keyname]
  else
    @logger.debug "Generate a new rsa key"
    key = OpenSSL::PKey::RSA.new 2048
    type = key.ssh_type
    data = [ key.to_blob ].pack('m0')
    @logger.debug "Creating Openstack keypair for public key '#{type} #{data}'"
    @compute_client.create_key_pair host[:vmhostname], "#{type} #{data}"
    host['ssh'][:key_data] = [ key.to_pem ]
    host[:vmhostname]
  end
end

#network(n) ⇒ String

Provided a network name return the OpenStack id for that network

Parameters:

  • n (String)

    The network name

Returns:

  • (String)

    Openstack id for provided network name



87
88
89
90
# File 'lib/beaker/hypervisor/openstack.rb', line 87

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

#provisionObject

Create new instances in OpenStack



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/beaker/hypervisor/openstack.rb', line 195

def provision
  @logger.notify "Provisioning OpenStack"

  @hosts.each do |host|
    ip = get_ip
    host[:vmhostname] = ip.ip.gsub '.','-'
    host[:vmfqdn] = host[:vmhostname] + '.rfc1918.puppetlabs.net'
    host[:keyname] = key_name(host)
    @logger.debug "Provisioning #{host.name} (#{host[:vmfqdn]})"
    options = {
      :flavor_ref => flavor(host[:flavor]).id,
      :image_ref  => image(host[:image]).id,
      :nics       => [ {'net_id' => network(@options[:openstack_network]).id } ],
      :name       => host[:vmfqdn],
      :hostname   => host[:vmfqdn],
      :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
    start = Time.now
    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[:vmfqdn]})"
          raise e
        end
        @logger.debug "Timeout connecting to instance #{host.name} (#{host[:vmfqdn]}), trying again..."
      end
      sleep SLEEPWAIT
      try += 1
    end

    # Associate a public IP to the server
    ip.server = vm
    host[:ip] = ip.ip

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

    #set metadata
    vm..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)
  end

  hack_etc_hosts @hosts, @options

end

#provision_storage(host, vm) ⇒ Object

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)

Parameters:

  • host (Hash)

    thet current host defined in the nodeset

  • vm (Fog::Compute::OpenStack::Server)

    the server to attach to



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/beaker/hypervisor/openstack.rb', line 135

def provision_storage host, vm
  if host['volumes']
    # Lazily create the volume client if needed
    volume_client_create
    host['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 = host['volumes'][volume]['size'].to_i / 1000

      # Create the volume and wait for it to become available
      vol = @volume_client.volumes.create(
        :size         => openstack_size,
        :display_name => volume,
        :description  => "Beaker volume: host=#{host.name} volume=#{volume}",
      )
      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) ⇒ Array

Provided an array of security groups return that array if all security groups are present

Parameters:

  • sgs (Array)

    The array of security group names

Returns:

  • (Array)

    The array of security group names



96
97
98
99
100
101
102
# File 'lib/beaker/hypervisor/openstack.rb', line 96

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_createFog::OpenStack::Volume

Create a volume client on request

Returns:

  • (Fog::OpenStack::Volume)

    OpenStack volume client



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/beaker/hypervisor/openstack.rb', line 106

def volume_client_create
  options = {
    :provider           => :openstack,
    :openstack_api_key  => @options[:openstack_api_key],
    :openstack_username => @options[:openstack_username],
    :openstack_auth_url => @options[:openstack_auth_url],
    :openstack_tenant   => @options[:openstack_tenant],
    :openstack_region   => @options[:openstack_region],
  }
  @volume_client ||= Fog::Volume.new(options)
  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