class Beaker::Host
Constants
- SELECT_TIMEOUT
Attributes
Public Class Methods
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
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
Source
# File lib/beaker/host.rb, line 143 def [] k host_hash[k] || options[k] end
Does this host have this key? Either as defined in the host itself, or globally?
Source
# File lib/beaker/host.rb, line 563 def add_env_var(key, val) raise NotImplementedError end
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
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
Source
# 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
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
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
Source
# 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
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
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
Source
# 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
Returns true if the host is running in FIPS mode.
Source
# 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
Determine the ip address of this host
Source
# 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
Determine the ip address using logic specific to the hypervisor
Source
# File lib/beaker/host.rb, line 148 def has_key? k host_hash.has_key?(k) || options.has_key?(k) end
Does this host have this key? Either as defined in the host itself, or globally?
Source
# File lib/beaker/host.rb, line 168 def hostname host_hash['vmhostname'] || @name end
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.
Source
# File lib/beaker/host.rb, line 559 def install_package(package, cmdline_args = nil, _version = nil, opts = {}) raise NotImplementedError end
Source
# File lib/beaker/host.rb, line 240 def ip self['ip'] = get_public_ip || get_ip end
Return the ip address of this host Always pull fresh, because this can sometimes change
Source
# File lib/beaker/host.rb, line 180 def is_cygwin? self.instance_of?(Windows::Host) end
Source
# File lib/beaker/host.rb, line 176 def is_pe? self['type'] && self['type'].to_s.include?('pe') end
Source
# File lib/beaker/host.rb, line 184 def is_powershell? self.instance_of?(PSWindows::Host) end
Source
# File lib/beaker/host.rb, line 245 def is_x86_64? @x86_64 ||= determine_if_x86_64 end
@return [Boolean] true if x86_64, false otherwise
Source
# File lib/beaker/host.rb, line 205 def log_prefix if host_hash['vmhostname'] "#{self} (#{@name})" else self.to_s end end
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
Source
# File lib/beaker/host.rb, line 551 def path_split(paths) raise NotImplementedError end
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
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
Source
# File lib/beaker/host.rb, line 133 def puppet_configprint(command = 'agent') PuppetConfigReader.new(self, command) end
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’)‘
Source
# File lib/beaker/host.rb, line 125 def reachable_name self['ip'] || hostname end
Return the preferred method to reach the host, will use IP is available and then default to {#hostname}.
Source
# File lib/beaker/host.rb, line 555 def rm_rf(path) raise NotImplementedError end
Source
# File lib/beaker/host.rb, line 547 def tmpdir(name = '') raise NotImplementedError end
Source
# File lib/beaker/host.rb, line 543 def tmpfile(name = '', extension = nil) raise NotImplementedError end
Source
# File lib/beaker/host.rb, line 162 def to_s hostname end
The {#hostname} of this host.
Source
# File lib/beaker/host.rb, line 157 def to_str hostname end
The {#hostname} of this host.
Source
# File lib/beaker/host.rb, line 115 def up? begin Socket.getaddrinfo(reachable_name, nil) return true rescue SocketError return false end end
Source
# 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
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()