class ChefCore::TargetHost

Constants

SSH_CONFIG_OVERRIDE_KEYS

These values may exist in .ssh/config but will be ignored by train in favor of its defaults unless we specify them explicitly. See apply_ssh_config

Attributes

backend[R]
config[R]
reporter[R]
transport_type[R]

Public Class Methods

mock_instance(url, family: "unknown", name: "unknown", release: "unknown", arch: "x86_64") click to toggle source

We're borrowing a page from train here - because setting up a reliable connection for testing is a multi-step process, we'll provide this method which instantiates a TargetHost connected to a train mock backend. If the family/name provided resolves to a suported OS, this instance will mix-in the supporting methods for the given platform; otherwise those methods will raise NotImplementedError.

# File lib/chef_core/target_host.rb, line 36
def self.mock_instance(url, family: "unknown", name: "unknown",
  release: "unknown", arch: "x86_64")
  # Specifying sudo: false ensures that attempted operations
  # don't fail because the mock platform doesn't support sudo
  target_host = TargetHost.new(url, { sudo: false })

  # Don't pull in the platform-specific mixins automatically during connect
  # Otherwise, it will raise since it can't resolve the OS without the mock.
  target_host.instance_variable_set(:@mocked_connection, true)
  target_host.connect!

  # We need to provide this mock before invoking mix_in_target_platform,
  # otherwise it will fail with an unknown OS (since we don't have a real connection).
  target_host.backend.mock_os(
    family: family,
    name: name,
    release: release,
    arch: arch
  )

  # Only mix-in if we can identify the platform.  This
  # prevents mix_in_target_platform! from raising on unknown platform during
  # tests that validate unsupported platform behaviors.
  if target_host.base_os != :other
    target_host.mix_in_target_platform!
  end

  target_host
end
new(host_url, opts = {}, logger = nil) click to toggle source
# File lib/chef_core/target_host.rb, line 66
def initialize(host_url, opts = {}, logger = nil)
  @config = connection_config(host_url, opts, logger)
  @transport_type = Train.validate_backend(@config)
  apply_ssh_config(@config, opts) if @transport_type == "ssh"
  @train_connection = Train.create(@transport_type, config)
end

Public Instance Methods

apply_ssh_config(config, opts_in) click to toggle source
# File lib/chef_core/target_host.rb, line 111
def apply_ssh_config(config, opts_in)
  # If we don't provide certain options, they will be defaulted
  # within train - in the case of ssh, this will prevent the .ssh/config
  # values from being picked up.
  # Here we'll modify the returned @config to specify
  # values that we get out of .ssh/config if present and if they haven't
  # been explicitly given.
  host_cfg = ssh_config_for_host(config[:host])
  SSH_CONFIG_OVERRIDE_KEYS.each do |key|
    if host_cfg.key?(key) && opts_in[key].nil?
      config[key] = host_cfg[key]
    end
  end
end
architecture() click to toggle source
# File lib/chef_core/target_host.rb, line 173
def architecture
  platform.arch
end
base_os() click to toggle source
# File lib/chef_core/target_host.rb, line 181
def base_os
  if platform.windows?
    :windows
  elsif platform.linux?
    :linux
  else
    :other
  end
end
chown(path, owner) click to toggle source

Simplified chown - just sets user, defaults to connection user. Does not touch group. Only has effect on non-windows targets

# File lib/chef_core/target_host.rb, line 287
def chown(path, owner); raise NotImplementedError; end
connect!() click to toggle source

Establish connection to configured target.

# File lib/chef_core/target_host.rb, line 128
def connect!
  # Keep existing connections
  return unless @backend.nil?

  @backend = train_connection.connection
  @backend.wait_until_ready

  # When the testing function `mock_instance` is used, it will set
  # this instance variable to false and handle this function call
  # of mixin functions based on the mocked platform.
  mix_in_target_platform! unless @mocked_connection
rescue Train::UserError => e
  raise ConnectionFailure.new(e, config)
rescue Train::Error => e
  # These are typically wrapper errors for other problems,
  # so we'll prefer to use e.cause over e if available.
  raise ConnectionFailure.new(e.cause || e, config)
end
connection_config(host_url, opts_in, logger) click to toggle source
# File lib/chef_core/target_host.rb, line 73
def connection_config(host_url, opts_in, logger)
  connection_opts = { target: host_url,
                      sudo: opts_in[:sudo] === false ? false : true,
                      www_form_encoded_password: true,
                      key_files: opts_in[:identity_file] || opts_in[:key_files],
                      non_interactive: true, # Prevent password prompts
                      connection_retries: 2,
                      connection_retry_sleep: 1,
                      logger: opts_in[:logger] || ChefCore::Log }

  target_opts = Train.unpack_target_from_uri(host_url)
  if opts_in.key?(:ssl) && opts_in[:ssl]
    connection_opts[:ssl] = opts_in[:ssl]
    connection_opts[:self_signed] = opts_in[:self_signed] || (opts_in[:ssl_verify] === false ? true : false)
  end

  target_opts[:host] = host_url if target_opts[:host].nil?
  target_opts[:backend] = "ssh" if target_opts[:backend].nil?
  connection_opts = connection_opts.merge(target_opts)

  # From WinRM gem: It is recommended that you :disable_sspi => true if you are using the plaintext or ssl transport.
  #                 See note here: https://github.com/mwrock/WinRM#example
  if %w{ssl plaintext}.include?(target_opts[:winrm_transport])
    target_opts[:winrm_disable_sspi] = true
  end

  connection_opts = connection_opts.merge(target_opts)

  # Anything we haven't explicitly set already, pass through to train.
  Train.options(target_opts[:backend]).keys.each do |key|
    if opts_in.key?(key) && !connection_opts.key?(key)
      connection_opts[key] = opts_in[key]
    end
  end

  Train.target_config(connection_opts)
end
del_dir(path) click to toggle source

Recursively delete directory

# File lib/chef_core/target_host.rb, line 295
def del_dir(path); raise NotImplementedError; end
del_file(path) click to toggle source
# File lib/chef_core/target_host.rb, line 297
def del_file(path); raise NotImplementedError; end
fetch_file_contents(remote_path) click to toggle source

Retrieve the contents of a remote file. Returns nil if the file didn't exist or couldn't be read.

# File lib/chef_core/target_host.rb, line 226
def fetch_file_contents(remote_path)
  result = backend.file(remote_path)
  if result.exist? && result.file?
    result.content
  else
    nil
  end
end
hostname() click to toggle source
# File lib/chef_core/target_host.rb, line 169
def hostname
  config[:host]
end
install_package(target_package_path) click to toggle source

Platform-specific installation of packages

# File lib/chef_core/target_host.rb, line 290
def install_package(target_package_path); raise NotImplementedError; end
installed_chef_version() click to toggle source

Returns the installed chef version as a Gem::Version, or raised ChefNotInstalled if chef client version manifest can't be found.

# File lib/chef_core/target_host.rb, line 238
def installed_chef_version
  return @installed_chef_version if @installed_chef_version

  # Note: In the case of a very old version of chef (that has no manifest - pre 12.0?)
  #       this will report as not installed.
  manifest = read_chef_version_manifest

  # We split the version here because  unstable builds install from)
  # are in the form "Major.Minor.Build+HASH" which is not a valid
  # version string.
  @installed_chef_version = Gem::Version.new(manifest["build_version"].split("+")[0])
end
make_directory(path) click to toggle source

create a directory. because we run all commands as root, this will also set group:owner to the connecting user if host isn't windows so that scp – which uses the connecting user – will have permissions to upload into it.

# File lib/chef_core/target_host.rb, line 274
def make_directory(path)
  mkdir(path)
  chown(path, user)
  path
end
mix_in_target_platform!() click to toggle source
# File lib/chef_core/target_host.rb, line 147
def mix_in_target_platform!
  case base_os
  when :linux
    require "chef_core/target_host/linux"
    class << self; include ChefCore::TargetHost::Linux; end
  when :windows
    require "chef_core/target_host/windows"
    class << self; include ChefCore::TargetHost::Windows; end
  when :other
    raise ChefCore::TargetHost::UnsupportedTargetOS.new(platform.name)
  end
end
normalize_path(p) click to toggle source

normalizes path across OS's

# File lib/chef_core/target_host.rb, line 281
def normalize_path(p) # NOTE BOOTSTRAP: was action::base::escape_windows_path
  p.tr("\\", "/")
end
omnibus_manifest_path() click to toggle source
# File lib/chef_core/target_host.rb, line 299
def omnibus_manifest_path(); raise NotImplementedError; end
platform() click to toggle source

TODO 2019-01-29 not expose this, it's internal implemenation. Same with backend.

# File lib/chef_core/target_host.rb, line 192
def platform
  backend.platform
end
read_chef_version_manifest() click to toggle source
# File lib/chef_core/target_host.rb, line 251
def read_chef_version_manifest
  manifest = fetch_file_contents(omnibus_manifest_path)
  raise ChefNotInstalled.new if manifest.nil?

  JSON.parse(manifest)
end
run_command(command, &data_handler) click to toggle source
# File lib/chef_core/target_host.rb, line 205
def run_command(command, &data_handler)
  backend.run_command command, &data_handler
end
run_command!(command, &data_handler) click to toggle source
# File lib/chef_core/target_host.rb, line 196
def run_command!(command, &data_handler)
  result = run_command(command, &data_handler)
  if result.exit_status != 0
    raise RemoteExecutionFailed.new(@config[:host], command, result)
  end

  result
end
save_as_remote_file(content, remote_path) click to toggle source

TODO spec

# File lib/chef_core/target_host.rb, line 210
def save_as_remote_file(content, remote_path)
  t = Tempfile.new("chef-content")
  t << content
  t.close
  upload_file(t.path, remote_path)
ensure
  t.close
  t.unlink
end
temp_dir() click to toggle source

Creates and caches location of temporary directory on the remote host using platform-specific implementations of make_temp_dir This will also set ownership to the connecting user instead of default of root when sudo'd, so that the dir can be used to upload files using scp as the connecting user.

The base temp dir is cached and will only be created once per connection lifetime.

# File lib/chef_core/target_host.rb, line 265
def temp_dir
  dir = make_temp_dir
  chown(dir, user)
  dir
end
upload_file(local_path, remote_path) click to toggle source
# File lib/chef_core/target_host.rb, line 220
def upload_file(local_path, remote_path)
  backend.upload(local_path, remote_path)
end
user() click to toggle source

Returns the user being used to connect. Defaults to train's default user if not specified

# File lib/chef_core/target_host.rb, line 161
def user
  return config[:user] unless config[:user].nil?

  require "train/transports/ssh"
  # TODO - this should use the right transport, not default to SSH
  Train::Transports::SSH.default_options[:user][:default]
end
version() click to toggle source
# File lib/chef_core/target_host.rb, line 177
def version
  platform.release
end
ws_cache_path() click to toggle source
# File lib/chef_core/target_host.rb, line 292
def ws_cache_path; raise NotImplementedError; end

Private Instance Methods

ssh_config_for_host(host) click to toggle source
# File lib/chef_core/target_host.rb, line 307
def ssh_config_for_host(host)
  require "net/ssh"
  Net::SSH::Config.for(host)
end
train_connection() click to toggle source
# File lib/chef_core/target_host.rb, line 303
def train_connection
  @train_connection
end