diff --git a/README.md b/README.md index bcf5755..180472d 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ CONFIG: openstack_volume_support: security_group: ['default'] preserve_hosts: + create_in_parallel: true run_in_parallel: ['configure', 'install'] type: ``` @@ -154,6 +155,11 @@ Further, you can opt to use a static master by setting the master's hypervisor t ip: ``` +Additionally, you can set instance creation to occur in parallel instead of sequentially via this CONFIG entry: +```yaml +create_in_parallel: true +``` + Additional parameter information is available at https://github.com/voxpupuli/beaker/blob/master/docs/concepts/argument_processing_and_precedence.md There is a simple rake task to invoke acceptance test for the library once the two environment variables are set: diff --git a/lib/beaker/hypervisor/openstack.rb b/lib/beaker/hypervisor/openstack.rb index 9ef1ac7..cfec9ba 100644 --- a/lib/beaker/hypervisor/openstack.rb +++ b/lib/beaker/hypervisor/openstack.rb @@ -250,86 +250,123 @@ def get_floating_ip ip end - #Create new instances in OpenStack + # Create new instances in OpenStack, depending on if create_in_parallel is true or not 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[:create_in_parallel] + # Enable abort on exception for threads + Thread.abort_on_exception = true + @logger.notify "Provisioning OpenStack in parallel" + provision_parallel + else + @logger.notify "Provisioning OpenStack sequentially" + provision_sequential + end + hack_etc_hosts @hosts, @options + 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"] + # Parallel creation wrapper + def provision_parallel + # Array to store threads + threads = @hosts.map do |host| + Thread.new do + create_instance_resources(host) end + end + # Wait for all threads to finish + threads.each(&:join) + 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) + # Sequential creation wrapper + def provision_sequential + @hosts.each do |host| + create_instance_resources(host) + end + end - #enable root if user is not root - enable_root(host) + # Create the actual instance resources + def create_instance_resources(host) + @logger.notify "Provisioning OpenStack" + 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 - 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] + 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 - hack_etc_hosts @hosts, @options + 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 the 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] + + # Handle exceptions in the thread + rescue => e + @logger.error "Thread #{host} failed with error: #{e.message}" + # Call cleanup function to delete orphaned hosts + cleanup + # Pass the error to the main thread to terminate all threads + Thread.main.raise(e) + # Terminate the current thread (to prevent hack_etc_hosts trying to run after error raised) + Thread.kill(Thread.current) end - #Destroy any OpenStack instances + # Destroy any OpenStack instances def cleanup @logger.notify "Cleaning up OpenStack" @vms.each do |vm| @@ -361,7 +398,7 @@ def enable_root_on_hosts end end - # enable root on a single host (the current one presumably) but only + # Enable root on a single host (the current one presumably) but only # if the username isn't 'root' def enable_root(host) if host['user'] != 'root'