class Beaker::Host

Constants

SELECT_TIMEOUT

Attributes

host_hash[R]
logger[RW]
name[R]
options[R]

Public Class Methods

create(name, host_hash, options) click to toggle source
# File lib/beaker/host.rb, line 45
def self.create name, host_hash, options
  case host_hash['platform']
  when /windows/
    cygwin = host_hash['is_cygwin']
    if cygwin.nil? or cygwin == true
      Windows::Host.new name, host_hash, options
    else
      PSWindows::Host.new name, host_hash, options
    end
  when /aix/
    Aix::Host.new name, host_hash, options
  when /osx/
    Mac::Host.new name, host_hash, options
  when /freebsd/
    FreeBSD::Host.new name, host_hash, options
  else
    Unix::Host.new name, host_hash, options
  end
end
new(name, host_hash, options) click to toggle source
# File lib/beaker/host.rb, line 68
def initialize name, host_hash, options
  @logger = host_hash[:logger] || options[:logger]
  @name, @host_hash, @options = name.to_s, host_hash.dup, options.dup
  @host_hash['packaging_platform'] ||= @host_hash['platform']

  @host_hash = self.platform_defaults.merge(@host_hash)
  pkg_initialize
end

Public Instance Methods

+(other) click to toggle source
# File lib/beaker/host.rb, line 172
def + other
  @name + other
end
[](k) click to toggle source

Does this host have this key? Either as defined in the host itself, or globally?

# File lib/beaker/host.rb, line 143
def [] k
  host_hash[k] || options[k]
end
[]=(k, v) click to toggle source
# File lib/beaker/host.rb, line 138
def []= k, v
  host_hash[k] = v
end
add_env_var(key, val) click to toggle source
# File lib/beaker/host.rb, line 563
def add_env_var(key, val)
  raise NotImplementedError
end
close() click to toggle source
# File lib/beaker/host.rb, line 266
def close
  if @connection
    @connection.close
    # update connection information
    @connection.ip         = self['ip'] if self['ip']
    @connection.vmhostname = self['vmhostname'] if self['vmhostname']
    @connection.hostname   = @name
  end
  @connection = nil
end
connection() click to toggle source
# File lib/beaker/host.rb, line 249
def connection
  # create new connection object if necessary
  if self['hypervisor'] == 'none' && @name == 'localhost'
    @connection ||= LocalConnection.connect({ :ssh_env_file => self['ssh_env_file'], :logger => @logger })
    return @connection
  end

  @connection ||= SshConnection.connect({ :ip => self['ip'], :vmhostname => self['vmhostname'], :hostname => @name },
                                        self['user'],
                                        self['ssh'], { :logger => @logger, :ssh_connection_preference => self[:ssh_connection_preference] })
  # update connection information
  @connection.ip = self['ip'] if self['ip'] && (@connection.ip != self['ip'])
  @connection.vmhostname = self['vmhostname'] if self['vmhostname'] && (@connection.vmhostname != self['vmhostname'])
  @connection.hostname = @name if @name && (@connection.hostname != @name)
  @connection
end
delete(k) click to toggle source
# File lib/beaker/host.rb, line 152
def delete k
  host_hash.delete(k)
end
do_rsync_to(from_path, to_path, opts = {}) click to toggle source

rsync a file or directory from the localhost to this test host @param from_path [String] The path to the file/dir to upload @param to_path [String] The destination path on the host @param opts [Hash{Symbol=>String}] Options to alter execution @option opts [Array<String>] :ignore An array of file/dir paths that will not be copied to the host @raise [Beaker::Host::CommandFailure] With Rsync error (if available) @return [Rsync::Result] Rsync result with status code

# File lib/beaker/host.rb, line 478
def do_rsync_to from_path, to_path, opts = {}
  ssh_opts = self['ssh']
  rsync_args = []
  ssh_args = []

  raise IOError, "No such file or directory - #{from_path}" if not File.file?(from_path) and not File.directory?(from_path)

  # We enable achieve mode and compression
  rsync_args << "-az"

  user = self['user'] || 'root'
  hostname_with_user = "#{user}@#{reachable_name}"

  Rsync.host = hostname_with_user

  # vagrant uses temporary ssh configs in order to use dynamic keys
  # without this config option using ssh may prompt for password
  #
  # We still want any user-set SSH config to win though
  filesystem_ssh_config = nil
  if ssh_opts[:config] && File.exist?(ssh_opts[:config])
    filesystem_ssh_config = ssh_opts[:config]
  elsif self[:vagrant_ssh_config] && File.exist?(self[:vagrant_ssh_config])
    filesystem_ssh_config = self[:vagrant_ssh_config]
  end

  if filesystem_ssh_config
    ssh_args << "-F #{filesystem_ssh_config}"
  elsif ssh_opts.has_key?('keys') and
        ssh_opts.has_key?('auth_methods') and
        ssh_opts['auth_methods'].include?('publickey')
    key = Array(ssh_opts['keys']).find do |k|
      File.exist?(k)
    end

    if key
      # rsync doesn't always play nice with tilde, so be sure to expand first
      ssh_args << "-i #{File.expand_path(key)}"
    end

    # find the first SSH key that exists
  end

  ssh_args << "-p #{ssh_opts[:port]}" if ssh_opts.has_key?(:port)

  # We disable prompt when host isn't known
  ssh_args << "-o 'StrictHostKeyChecking no'"

  rsync_args << "-e \"ssh #{ssh_args.join(' ')}\"" if not ssh_args.empty?

  rsync_args << opts[:ignore].map { |value| "--exclude '#{value}'" }.join(' ') if opts.has_key?(:ignore) and not opts[:ignore].empty?

  # We assume that the *contents* of the directory 'from_path' needs to be
  # copied into the directory 'to_path'
  from_path += '/' if File.directory?(from_path) and not from_path.end_with?('/')

  @logger.notify "rsync: localhost:#{from_path} to #{hostname_with_user}:#{to_path} {:ignore => #{opts[:ignore]}}"
  result = Rsync.run(from_path, to_path, rsync_args)
  @logger.debug("rsync returned #{result.inspect}")

  return result if result.success?

  raise Beaker::Host::CommandFailure, result.error
end
do_scp_from(source, target, options) click to toggle source
# File lib/beaker/host.rb, line 454
def do_scp_from source, target, options
  # use the value of :dry_run passed to the method unless
  # undefined, then use parsed @options hash.
  options[:dry_run] ||= @options[:dry_run]

  if options[:dry_run]
    scp_cmd = "scp #{@name}:#{source} #{target}"
    @logger.debug "\n Running in :dry_run mode. localhost $ #{scp_cmd} not executed."
    return NullResult.new(self, scp_cmd)
  end

  @logger.debug "localhost $ scp #{@name}:#{source} #{target}"
  result = connection.scp_from(source, target, options)
  @logger.debug result.stdout
  return result
end
do_scp_to(source, target_path, options) click to toggle source

scp files from the localhost to this test host, if a directory is provided it is recursively copied. If the provided source is a directory both the contents of the directory and the directory itself will be copied to the host, if you only want to copy directory contents you will either need to specify the contents file by file or do a separate ‘mv’ command post scp_to to create the directory structure as desired. To determine if a file/dir is ‘ignored’ we compare to any contents of the source dir and NOT any part of the path to that source dir.

@param source [String] The path to the file/dir to upload @param target_path [String] The destination path on the host @param options [Hash{Symbol=>String}] Options to alter execution @option options [Array<String>] :ignore An array of file/dir paths that will not be copied to the host @example

do_scp_to('source/dir1/dir2/dir3', 'target')
-> will result in creation of target/source/dir1/dir2/dir3 on host

do_scp_to('source/file.rb', 'target', { :ignore => 'file.rb' }
-> will result in not files copyed to the host, all are ignored
# File lib/beaker/host.rb, line 371
def do_scp_to source, target_path, options
  target = self.scp_path(target_path)

  # use the value of :dry_run passed to the method unless
  # undefined, then use parsed @options hash.
  options[:dry_run] ||= @options[:dry_run]

  if options[:dry_run]
    scp_cmd = "scp #{source} #{@name}:#{target}"
    @logger.debug "\n Running in :dry_run mode. localhost $ #{scp_cmd} not executed."
    return NullResult.new(self, scp_cmd)
  end

  @logger.notify "localhost $ scp #{source} #{@name}:#{target} {:ignore => #{options[:ignore]}}"

  result = Result.new(@name, [source, target])
  has_ignore = options[:ignore] and not options[:ignore].empty?
  # construct the regex for matching ignored files/dirs
  ignore_re = nil
  if has_ignore
    ignore_arr = Array(options[:ignore]).map do |entry|
      "((\/|\\A)#{Regexp.escape(entry)}(\/|\\z))"
    end
    ignore_re = Regexp.new(ignore_arr.join('|'))
    @logger.debug("going to ignore #{ignore_re}")
  end

  # either a single file, or a directory with no ignores
  raise IOError, "No such file or directory - #{source}" if not File.file?(source) and not File.directory?(source)

  if File.file?(source) or (File.directory?(source) and not has_ignore)
    source_file = source
    if has_ignore and ignore_re&.match?(source)
      @logger.trace "After rejecting ignored files/dirs, there is no file to copy"
      source_file = nil
      result.stdout = "No files to copy"
      result.exit_code = 1
    end
    if source_file
      result = connection.scp_to(source_file, target, options)
      @logger.trace result.stdout
    end
  else # a directory with ignores
    dir_source = Dir.glob("#{source}/**/*").reject do |f|
      ignore_re&.match?(f.gsub(/\A#{Regexp.escape(source)}/, '')) # only match against subdirs, not full path
    end
    @logger.trace "After rejecting ignored files/dirs, going to scp [#{dir_source.join(', ')}]"

    # create necessary directory structure on host
    # run this quietly (no STDOUT)
    @logger.quiet(true)
    required_dirs = (dir_source.map { |dir| File.dirname(dir) }).uniq
    require 'pathname'
    required_dirs.each do |dir|
      dir_path = Pathname.new(dir)
      if dir_path.absolute? and (File.dirname(File.absolute_path(source)).to_s != '/')
        mkdir_p(File.join(target, dir.gsub(/#{Regexp.escape(File.dirname(File.absolute_path(source)))}/, '')))
      else
        mkdir_p(File.join(target, dir))
      end
    end
    @logger.quiet(false)

    # copy each file to the host
    dir_source.each do |s|
      # Copy files, not directories (as they are copied recursively)
      next if File.directory?(s)

      s_path = Pathname.new(s)
      file_path = if s_path.absolute? and (File.dirname(File.absolute_path(source)).to_s != '/')
                    File.join(target, File.dirname(s).gsub(/#{Regexp.escape(File.dirname(File.absolute_path(source)))}/, ''))
                  else
                    File.join(target, File.dirname(s))
                  end
      result = connection.scp_to(s, file_path, options)
      @logger.trace result.stdout
    end
  end

  self.scp_post_operations(target, target_path)
  return result
end
exec(command, options = {}) click to toggle source
# File lib/beaker/host.rb, line 277
def exec command, options = {}
  result = nil
  # I've always found this confusing
  cmdline = command.cmd_line(self)

  # use the value of :dry_run passed to the method unless
  # undefined, then use parsed @options hash.
  options[:dry_run] ||= @options[:dry_run]

  if options[:dry_run]
    @logger.debug "\n Running in :dry_run mode. Command #{cmdline} not executed."
    result = Beaker::NullResult.new(self, command)
    return result
  end

  if options[:silent]
    output_callback = nil
  else
    @logger.debug "\n#{log_prefix} #{Time.new.strftime('%H:%M:%S')}$ #{cmdline}"
    output_callback = if @options[:color_host_output]
                        logger.method(:color_host_output)
                      else
                        logger.method(:host_output)
                      end
  end

  unless options[:dry_run]
    # is this returning a result object?
    # the options should come at the end of the method signature (rubyism)
    # and they shouldn't be ssh specific

    seconds = Benchmark.realtime do
      @logger.with_indent do
        result = connection.execute(cmdline, options, output_callback)
      end
    end

    @logger.debug "\n#{log_prefix} executed in %0.2f seconds" % seconds if not options[:silent]

    if options[:reset_connection]
      # Expect the connection to fail hard and possibly take a long time timeout.
      # Pre-emptively reset it so we don't wait forever.
      close
      return result
    end

    unless options[:silent]
      # What?
      result.log(@logger)
      if !options[:expect_connection_failure] && !result.exit_code
        # no exit code was collected, so the stream failed
        raise CommandFailure, "Host '#{self}' connection failure running:\n #{cmdline}\nLast #{@options[:trace_limit]} lines of output were:\n#{result.formatted_output(@options[:trace_limit])}"

      end

      if options[:expect_connection_failure] && result.exit_code
        # should have had a connection failure, but didn't
        # wait to see if the connection failure will be generation, otherwise raise error
        if not connection.wait_for_connection_failure(options, output_callback)
          raise CommandFailure, "Host '#{self}' should have resulted in a connection failure running:\n #{cmdline}\nLast #{@options[:trace_limit]} lines of output were:\n#{result.formatted_output(@options[:trace_limit])}"
        end
      end
      # No, TestCase has the knowledge about whether its failed, checking acceptable
      # exit codes at the host level and then raising...
      # is it necessary to break execution??
      if options[:accept_all_exit_codes] && options[:acceptable_exit_codes]
        @logger.warn ":accept_all_exit_codes & :acceptable_exit_codes set. :acceptable_exit_codes overrides, but they shouldn't both be set at once"
        options[:accept_all_exit_codes] = false
      end
      if !options[:accept_all_exit_codes] && !result.exit_code_in?(Array(options[:acceptable_exit_codes] || [0, nil]))
        raise CommandFailure, "Host '#{self}' exited with #{result.exit_code} running:\n #{cmdline}\nLast #{@options[:trace_limit]} lines of output were:\n#{result.formatted_output(@options[:trace_limit])}"
      end
    end
  end
  result
end
fips_mode?() click to toggle source

Returns true if the host is running in FIPS mode.

# File lib/beaker/host.rb, line 193
def fips_mode?
  if self.file_exist?('/proc/sys/crypto/fips_enabled')
    begin
      execute("cat /proc/sys/crypto/fips_enabled") == "1"
    rescue Beaker::Host::CommandFailure
      false
    end
  else
    false
  end
end
get_ip() click to toggle source

Determine the ip address of this host

# File lib/beaker/host.rb, line 214
def get_ip
  @logger.warn("Uh oh, this should be handled by sub-classes but hasn't been")
end
get_public_ip() click to toggle source

Determine the ip address using logic specific to the hypervisor

# File lib/beaker/host.rb, line 219
def get_public_ip
  case host_hash[:hypervisor]
  when /^(ec2|openstack)$/
    if self[:hypervisor] == 'ec2' && self[:instance]
      return self[:instance].ip_address
    elsif self[:hypervisor] == 'openstack' && self[:ip]
      return self[:ip]
    elsif self.instance_of?(Windows::Host)
      # In the case of using ec2 instances with the --no-provision flag, the ec2
      # instance object does not exist and we should just use the curl endpoint
      # specified here:
      # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-instance-addressing.html
      execute("wget http://169.254.169.254/latest/meta-data/public-ipv4").strip
    else
      execute("curl http://169.254.169.254/latest/meta-data/public-ipv4").strip
    end
  end
end
has_key?(k) click to toggle source

Does this host have this key? Either as defined in the host itself, or globally?

# File lib/beaker/host.rb, line 148
def has_key? k
  host_hash.has_key?(k) || options.has_key?(k)
end
hostname() click to toggle source

Return the public name of the particular host, which may be different then the name of the host provided in the configuration file as some provisioners create random, unique hostnames.

# File lib/beaker/host.rb, line 168
def hostname
  host_hash['vmhostname'] || @name
end
install_package(package, cmdline_args = nil, _version = nil, opts = {}) click to toggle source
# File lib/beaker/host.rb, line 559
def install_package(package, cmdline_args = nil, _version = nil, opts = {})
  raise NotImplementedError
end
ip() click to toggle source

Return the ip address of this host Always pull fresh, because this can sometimes change

# File lib/beaker/host.rb, line 240
def ip
  self['ip'] = get_public_ip || get_ip
end
is_cygwin?() click to toggle source
# File lib/beaker/host.rb, line 180
def is_cygwin?
  self.instance_of?(Windows::Host)
end
is_pe?() click to toggle source
# File lib/beaker/host.rb, line 176
def is_pe?
  self['type'] && self['type'].to_s.include?('pe')
end
is_powershell?() click to toggle source
# File lib/beaker/host.rb, line 184
def is_powershell?
  self.instance_of?(PSWindows::Host)
end
is_x86_64?() click to toggle source

@return [Boolean] true if x86_64, false otherwise

# File lib/beaker/host.rb, line 245
def is_x86_64?
  @x86_64 ||= determine_if_x86_64
end
log_prefix() click to toggle source
# File lib/beaker/host.rb, line 205
def log_prefix
  if host_hash['vmhostname']
    "#{self} (#{@name})"
  else
    self.to_s
  end
end
node_name() click to toggle source
# File lib/beaker/host.rb, line 82
def node_name
  # TODO: might want to consider caching here; not doing it for now because
  #  I haven't thought through all of the possible scenarios that could
  #  cause the value to change after it had been cached.
  puppet_configprint['node_name_value'].strip
end
path_split(paths) click to toggle source
# File lib/beaker/host.rb, line 551
def path_split(paths)
  raise NotImplementedError
end
pkg_initialize() click to toggle source
# File lib/beaker/host.rb, line 77
def pkg_initialize
  # This method should be overridden by platform-specific code to
  # handle whatever packaging-related initialization is necessary.
end
platform() click to toggle source
# File lib/beaker/host.rb, line 188
def platform
  self['platform']
end
port_open?(port) click to toggle source
# File lib/beaker/host.rb, line 89
def port_open? port
  begin
    Timeout.timeout SELECT_TIMEOUT do
      TCPSocket.new(reachable_name, port).close
      return true
    end
  rescue Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, Errno::EHOSTUNREACH
    return false
  end
end
puppet(command = 'agent')
Alias for: puppet_configprint
puppet_configprint(command = 'agent') click to toggle source

Returning our PuppetConfigReader here allows users of the Host class to do things like ‘host.puppet` to query the ’main’ section or, if they want the configuration for a particular run type, ‘host.puppet(’agent’)

# File lib/beaker/host.rb, line 133
def puppet_configprint(command = 'agent')
  PuppetConfigReader.new(self, command)
end
Also aliased as: puppet
reachable_name() click to toggle source

Return the preferred method to reach the host, will use IP is available and then default to {#hostname}.

# File lib/beaker/host.rb, line 125
def reachable_name
  self['ip'] || hostname
end
rm_rf(path) click to toggle source
# File lib/beaker/host.rb, line 555
def rm_rf(path)
  raise NotImplementedError
end
tmpdir(name = '') click to toggle source
# File lib/beaker/host.rb, line 547
def tmpdir(name = '')
  raise NotImplementedError
end
tmpfile(name = '', extension = nil) click to toggle source
# File lib/beaker/host.rb, line 543
def tmpfile(name = '', extension = nil)
  raise NotImplementedError
end
to_s() click to toggle source

The {#hostname} of this host.

# File lib/beaker/host.rb, line 162
def to_s
  hostname
end
to_str() click to toggle source

The {#hostname} of this host.

# File lib/beaker/host.rb, line 157
def to_str
  hostname
end
up?() click to toggle source
# File lib/beaker/host.rb, line 115
def up?
  begin
    Socket.getaddrinfo(reachable_name, nil)
    return true
  rescue SocketError
    return false
  end
end
wait_for_port(port, attempts = 15) click to toggle source

Wait for a port on the host. Useful for those occasions when you’ve called host.reboot and want to avoid spam from subsequent SSH connections retrying to connect from say retry_on()

# File lib/beaker/host.rb, line 103
def wait_for_port(port, attempts = 15)
  @logger.debug("  Waiting for port #{port} ... ", false)
  start = Time.now
  done = repeat_fibonacci_style_for(attempts) { port_open?(port) }
  if done
    @logger.debug(format('connected in %0.2f seconds', (Time.now - start)))
  else
    @logger.debug('timeout')
  end
  done
end