lib/beaker/hypervisor/google_compute.rb in beaker-google-0.3.0 vs lib/beaker/hypervisor/google_compute.rb in beaker-google-0.4.0

- old
+ new

@@ -1,39 +1,54 @@ require 'time' module Beaker - # Beaker support for the Google Compute Engine. class GoogleCompute < Beaker::Hypervisor - SLEEPWAIT = 5 # Hours before an instance is considered a zombie ZOMBIE = 3 # Do some reasonable sleuthing on the SSH public key for GCE - def find_google_ssh_public_key - keyfile = ENV.fetch('BEAKER_gce_ssh_public_key', File.join(ENV['HOME'], '.ssh', 'google_compute_engine.pub')) - if @options[:gce_ssh_public_key] && !File.exist?(keyfile) - keyfile = @options[:gce_ssh_public_key] - end + ## + # Try to find the private ssh key file + # + # @return [String] The file path for the private key file + # + # @raise [Error] if the private key can not be found + def find_google_ssh_private_key + private_keyfile = ENV.fetch('BEAKER_gce_ssh_public_key', + File.join(ENV.fetch('HOME', nil), '.ssh', 'google_compute_engine')) + private_keyfile = @options[:gce_ssh_private_key] if @options[:gce_ssh_private_key] && !File.exist?(private_keyfile) + raise("Could not find GCE Private SSH key at '#{keyfile}'") unless File.exist?(private_keyfile) + @options[:gce_ssh_private_key] = private_keyfile + private_keyfile + end - raise("Could not find GCE Public SSH Key at '#{keyfile}'") unless File.exist?(keyfile) - - return keyfile + ## + # Try to find the public key file based on the location of the private key or provided data + # + # @return [String] The file path for the public key file + # + # @raise [Error] if the public key can not be found + def find_google_ssh_public_key + private_keyfile = find_google_ssh_private_key + public_keyfile = private_keyfile << '.pub' + public_keyfile = @options[:gce_ssh_public_key] if @options[:gce_ssh_public_key] && !File.exist?(public_keyfile) + raise("Could not find GCE Public SSH key at '#{keyfile}'") unless File.exist?(public_keyfile) + @options[:gce_ssh_public_key] = public_keyfile + public_keyfile end - # Create the array of metaData, each member being a hash with a :key and a - # :value. Sets :department, :project and :jenkins_build_url. - def format_metadata - [ {:key => :department, :value => @options[:department]}, - {:key => :project, :value => @options[:project]}, - {:key => :jenkins_build_url, :value => @options[:jenkins_build_url]}, - {:key => :sshKeys, :value => "google_compute:#{File.read(find_google_ssh_public_key).strip}" } - ].delete_if { |member| member[:value].nil? or member[:value].empty?} + # IP is the only way we can be sure to connect + # TODO: This isn't being called + # rubocop:disable Lint/UnusedMethodArgument + def connection_preference(host) + [:ip] end + # rubocop:enable Lint/UnusedMethodArgument # Create a new instance of the Google Compute Engine hypervisor object # # @param [<Host>] google_hosts The Array of google hosts to provision, may # ONLY be of platforms /centos-*/, /debian-*/, /rhel-*/, /suse-*/. Only @@ -58,151 +73,155 @@ # @option options [Integer] :timeout The amount of time to attempt execution # before quiting and exiting with failure def initialize(google_hosts, options) require 'beaker/hypervisor/google_compute_helper' + super @options = options @logger = options[:logger] @hosts = google_hosts @firewall = '' @gce_helper = GoogleComputeHelper.new(options) end # Create and configure virtual machines in the Google Compute Engine, # including their associated disks and firewall rules def provision - attempts = @options[:timeout].to_i / SLEEPWAIT start = Time.now - test_group_identifier = "beaker-#{start.to_i}-" - # get machineType resource, used by all instances - machineType = @gce_helper.get_machineType(start, attempts) - # set firewall to open pe ports - network = @gce_helper.get_network(start, attempts) + network = @gce_helper.get_network + @firewall = test_group_identifier + generate_host_name - @gce_helper.create_firewall(@firewall, network, start, attempts) + @gce_helper.create_firewall(@firewall, network) + @logger.debug("Created Google Compute firewall #{@firewall}") - @hosts.each do |host| + + machine_type_name = ENV.fetch('BEAKER_gce_machine_type', host['gce_machine_type']) + raise "Must provide a machine type name in 'gce_machine_type'." if machine_type_name.nil? + # Get the GCE machine type object for this host + machine_type = @gce_helper.get_machine_type(machine_type_name) + raise "Unable to find machine type named #{machine_type_name} in region #{@compute.default_zone}" if machine_type.nil? + + # Find the image to use to create the new VM. + # Either `image` or `family` must be set in the configuration. Accepted formats + # for the image and family: + # - {project}/{image} + # - {project}/{family} + # - {image} + # - {family} + # + # If a {project} is not specified, default to the project provided in the + # BEAKER_gce_project environment variable if host[:image] - gplatform = host[:image] - elsif host[:platform] - gplatform = Platform.new(host[:platform]) + image_selector = host[:image] + # Do we have a project name? + if %r{/}.match?(image_selector) + image_project, image_name = image_selector.split('/')[0..1] + else + image_project = @gce_helper.options[:gce_project] + image_name = image_selector + end + img = @gce_helper.get_image(image_project, image_name) + raise "Unable to find image #{image_name} from project #{image_project}" if img.nil? + elsif host[:family] + image_selector = host[:family] + # Do we have a project name? + if %r{/}.match?(image_selector) + image_project, family_name = image_selector.split('/') + else + image_project = @gce_helper.options[:gce_project] + family_name = image_selector + end + img = @gce_helper.get_latest_image_from_family(image_project, family_name) + raise "Unable to find image in family #{family_name} from project #{image_project}" if img.nil? else - raise('You must specify either :image or :platform, or both as necessary') + raise('You must specify either :image or :family') end - img = @gce_helper.get_latest_image(gplatform, start, attempts) - unique_host_id = test_group_identifier + generate_host_name - host['diskname'] = unique_host_id - disk = @gce_helper.create_disk(host['diskname'], img, start, attempts) - @logger.debug("Created Google Compute disk for #{host.name}: #{host['diskname']}") + boot_size = host['volume_size'] || img.disk_size_gb + # The boot disk is created as part of the instance creation + # TODO: Allow creation of other disks + # disk = @gce_helper.create_disk(host["diskname"], img, size) + # @logger.debug("Created Google Compute disk for #{host.name}: #{host["diskname"]}") + # create new host name host['vmhostname'] = unique_host_id - #add a new instance of the image - instance = @gce_helper.create_instance(host['vmhostname'], img, machineType, disk, start, attempts) + + # add a new instance of the image + operation = @gce_helper.create_instance(host['vmhostname'], img, machine_type, boot_size) + unless operation.error.nil? + raise "Unable to create Google Compute Instance #{host.name}: [#{operation.error.errors[0].code}] #{operation.error.errors[0].message}" + end @logger.debug("Created Google Compute instance for #{host.name}: #{host['vmhostname']}") + instance = @gce_helper.get_instance(host['vmhostname']) # add metadata to instance, if there is any to set - mdata = format_metadata - unless mdata.empty? - @gce_helper.setMetadata_on_instance(host['vmhostname'], instance['metadata']['fingerprint'], - mdata, - start, attempts) - @logger.debug("Added tags to Google Compute instance #{host.name}: #{host['vmhostname']}") - end + # mdata = format_metadata + # TODO: Set a configuration option for this to allow disabeling oslogin + mdata = [ + { + key: 'ssh-keys', + value: "google_compute:#{File.read(find_google_ssh_public_key).strip}" + }, + # For now oslogin needs to be disabled as there's no way to log in as root and it would + # take too much work on beaker to add sudo support to everything + { + key: 'enable-oslogin', + value: 'FALSE' + }, + ] + next if mdata.empty? + # Add the metadata to the host + @gce_helper.set_metadata_on_instance(host['vmhostname'], mdata) + @logger.debug("Added tags to Google Compute instance #{host.name}: #{host['vmhostname']}") - # get ip for this host - host['ip'] = instance['networkInterfaces'][0]['accessConfigs'][0]['natIP'] + host['ip'] = instance.network_interfaces[0].access_configs[0].nat_ip - # configure ssh - default_user = host['user'] - host['user'] = 'google_compute' + # Add the new host to the firewall + @gce_helper.add_firewall_tag(@firewall, host['vmhostname']) - copy_ssh_to_root(host, @options) - enable_root_login(host, @options) - host['user'] = default_user + if host['disable_root_ssh'] == true + @logger.info('Not enabling root ssh as disable_root_ssh is true') + else - # shut down connection, will reconnect on next exec - host.close + # # configure ssh + default_user = host['user'] + # TODO: Pull this out into a configuration option or something + host['user'] = 'google_compute' + + # Set the ssh private key we need to use + host.options['ssh']['keys'] = [find_google_ssh_private_key] + + copy_ssh_to_root(host, @options) + enable_root_login(host, @options) + host['user'] = default_user + + # shut down connection, will reconnect on next exec + host.close + end + @logger.debug("Instance ready: #{host['vmhostname']} for #{host.name}}") end end # Shutdown and destroy virtual machines in the Google Compute Engine, # including their associated disks and firewall rules - def cleanup() - attempts = @options[:timeout].to_i / SLEEPWAIT - start = Time.now + def cleanup + @gce_helper.delete_firewall(@firewall) - @gce_helper.delete_firewall(@firewall, start, attempts) - @hosts.each do |host| - @gce_helper.delete_instance(host['vmhostname'], start, attempts) + # TODO: Delete any other disks attached during the instance creation + @gce_helper.delete_instance(host['vmhostname']) @logger.debug("Deleted Google Compute instance #{host['vmhostname']} for #{host.name}") - @gce_helper.delete_disk(host['diskname'], start, attempts) - @logger.debug("Deleted Google Compute disk #{host['diskname']} for #{host.name}") - end - - end - - # Shutdown and destroy Google Compute instances (including their associated - # disks and firewall rules) that have been alive longer than ZOMBIE hours. - def kill_zombies(max_age = ZOMBIE) - now = start = Time.now - attempts = @options[:timeout].to_i / SLEEPWAIT - - # get rid of old instances - instances = @gce_helper.list_instances(start, attempts) - if instances - instances.each do |instance| - created = Time.parse(instance['creationTimestamp']) - alive = (now - created )/60/60 - if alive >= max_age - #kill it with fire! - @logger.debug("Deleting zombie instance #{instance['name']}") - @gce_helper.delete_instance( instance['name'], start, attempts ) - end - end - else - @logger.debug("No zombie instances found") - end - - # get rid of old disks - disks = @gce_helper.list_disks(start, attempts) - if disks - disks.each do |disk| - created = Time.parse(disk['creationTimestamp']) - alive = (now - created )/60/60 - if alive >= max_age - - # kill it with fire! - @logger.debug("Deleting zombie disk #{disk['name']}") - @gce_helper.delete_disk( disk['name'], start, attempts ) - end - end - else - @logger.debug("No zombie disks found") - end - - # get rid of non-default firewalls - firewalls = @gce_helper.list_firewalls( start, attempts) - - if firewalls && !firewalls.empty? - firewalls.each do |firewall| - @logger.debug("Deleting non-default firewall #{firewall['name']}") - @gce_helper.delete_firewall( firewall['name'], start, attempts ) - end - else - @logger.debug("No zombie firewalls found") end end end end