Class: ChefCore::TargetHost

Inherits:
Object
  • Object
show all
Defined in:
lib/chef_core/target_host.rb,
lib/chef_core/target_host/linux.rb,
lib/chef_core/target_host/windows.rb

Defined Under Namespace

Modules: Linux, Windows Classes: ChefNotInstalled, ConnectionFailure, RemoteExecutionFailed, UnsupportedTargetOS

Constant Summary collapse

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

[:user, :port, :proxy].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host_url, opts = {}, logger = nil) ⇒ TargetHost

Returns a new instance of TargetHost.



66
67
68
69
70
71
# 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

Instance Attribute Details

#backendObject (readonly)

Returns the value of attribute backend.



24
25
26
# File 'lib/chef_core/target_host.rb', line 24

def backend
  @backend
end

#configObject (readonly)

Returns the value of attribute config.



24
25
26
# File 'lib/chef_core/target_host.rb', line 24

def config
  @config
end

#reporterObject (readonly)

Returns the value of attribute reporter.



24
25
26
# File 'lib/chef_core/target_host.rb', line 24

def reporter
  @reporter
end

#transport_typeObject (readonly)

Returns the value of attribute transport_type.



24
25
26
# File 'lib/chef_core/target_host.rb', line 24

def transport_type
  @transport_type
end

Class Method Details

.mock_instance(url, family: "unknown", name: "unknown", release: "unknown", arch: "x86_64") ⇒ Object

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.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 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

Instance Method Details

#apply_ssh_config(config, opts_in) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
# 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

#architectureObject



171
172
173
# File 'lib/chef_core/target_host.rb', line 171

def architecture
  platform.arch
end

#base_osObject



179
180
181
182
183
184
185
186
187
# File 'lib/chef_core/target_host.rb', line 179

def base_os
  if platform.windows?
    :windows
  elsif platform.linux?
    :linux
  else
    :other
  end
end

#chown(path, owner) ⇒ Object

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

Raises:

  • (NotImplementedError)


282
# File 'lib/chef_core/target_host.rb', line 282

def chown(path, owner); raise NotImplementedError; end

#connect!Object

Establish connection to configured target.



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 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 ["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) ⇒ Object

Recursively delete directory

Raises:

  • (NotImplementedError)


290
# File 'lib/chef_core/target_host.rb', line 290

def del_dir(path); raise NotImplementedError; end

#del_file(path) ⇒ Object

Raises:

  • (NotImplementedError)


292
# File 'lib/chef_core/target_host.rb', line 292

def del_file(path); raise NotImplementedError; end

#fetch_file_contents(remote_path) ⇒ Object

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



223
224
225
226
227
228
229
230
# File 'lib/chef_core/target_host.rb', line 223

def fetch_file_contents(remote_path)
  result = backend.file(remote_path)
  if result.exist? && result.file?
    result.content
  else
    nil
  end
end

#hostnameObject



167
168
169
# File 'lib/chef_core/target_host.rb', line 167

def hostname
  config[:host]
end

#install_package(target_package_path) ⇒ Object

Platform-specific installation of packages

Raises:

  • (NotImplementedError)


285
# File 'lib/chef_core/target_host.rb', line 285

def install_package(target_package_path); raise NotImplementedError; end

#installed_chef_versionObject

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



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/chef_core/target_host.rb', line 235

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) ⇒ Object

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.



269
270
271
272
273
# File 'lib/chef_core/target_host.rb', line 269

def make_directory(path)
  mkdir(path)
  chown(path, user)
  path
end

#mix_in_target_platform!Object



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/chef_core/target_host.rb', line 146

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) ⇒ Object

normalizes path across OS’s



276
277
278
# File 'lib/chef_core/target_host.rb', line 276

def normalize_path(p) # NOTE BOOTSTRAP: was action::base::escape_windows_path
  p.tr("\\", "/")
end

#omnibus_manifest_pathObject

Raises:

  • (NotImplementedError)


294
# File 'lib/chef_core/target_host.rb', line 294

def omnibus_manifest_path(); raise NotImplementedError; end

#platformObject

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



190
191
192
# File 'lib/chef_core/target_host.rb', line 190

def platform
  backend.platform
end

#read_chef_version_manifestObject

Raises:



247
248
249
250
251
# File 'lib/chef_core/target_host.rb', line 247

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) ⇒ Object



202
203
204
# File 'lib/chef_core/target_host.rb', line 202

def run_command(command, &data_handler)
  backend.run_command command, &data_handler
end

#run_command!(command, &data_handler) ⇒ Object



194
195
196
197
198
199
200
# File 'lib/chef_core/target_host.rb', line 194

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) ⇒ Object

TODO spec



207
208
209
210
211
212
213
214
215
# File 'lib/chef_core/target_host.rb', line 207

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_dirObject

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.



260
261
262
263
264
# File 'lib/chef_core/target_host.rb', line 260

def temp_dir
  dir = make_temp_dir()
  chown(dir, user)
  dir
end

#upload_file(local_path, remote_path) ⇒ Object



217
218
219
# File 'lib/chef_core/target_host.rb', line 217

def upload_file(local_path, remote_path)
  backend.upload(local_path, remote_path)
end

#userObject

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



160
161
162
163
164
165
# File 'lib/chef_core/target_host.rb', line 160

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

#versionObject



175
176
177
# File 'lib/chef_core/target_host.rb', line 175

def version
  platform.release
end

#ws_cache_pathObject

Raises:

  • (NotImplementedError)


287
# File 'lib/chef_core/target_host.rb', line 287

def ws_cache_path; raise NotImplementedError; end