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

%i{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



173
174
175
# File 'lib/chef_core/target_host.rb', line 173

def architecture
  platform.arch
end

#base_osObject



181
182
183
184
185
186
187
188
189
# 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) ⇒ Object

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

Raises:

  • (NotImplementedError)


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

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
145
# 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 %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) ⇒ Object

Recursively delete directory

Raises:

  • (NotImplementedError)


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

def del_dir(path); raise NotImplementedError; end

#del_file(path) ⇒ Object

Raises:

  • (NotImplementedError)


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

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.



226
227
228
229
230
231
232
233
# 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

#hostnameObject



169
170
171
# File 'lib/chef_core/target_host.rb', line 169

def hostname
  config[:host]
end

#install_package(target_package_path) ⇒ Object

Platform-specific installation of packages

Raises:

  • (NotImplementedError)


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

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.



238
239
240
241
242
243
244
245
246
247
248
249
# 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) ⇒ 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.



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

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

#mix_in_target_platform!Object



147
148
149
150
151
152
153
154
155
156
157
158
# 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) ⇒ Object

normalizes path across OS’s



281
282
283
# 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_pathObject

Raises:

  • (NotImplementedError)


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

def omnibus_manifest_path(); raise NotImplementedError; end

#platformObject

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



192
193
194
# File 'lib/chef_core/target_host.rb', line 192

def platform
  backend.platform
end

#read_chef_version_manifestObject

Raises:



251
252
253
254
255
256
# 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) ⇒ Object



205
206
207
# 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) ⇒ Object



196
197
198
199
200
201
202
203
# 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) ⇒ Object

TODO spec



210
211
212
213
214
215
216
217
218
# 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_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.



265
266
267
268
269
# 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) ⇒ Object



220
221
222
# File 'lib/chef_core/target_host.rb', line 220

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



161
162
163
164
165
166
167
# 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

#versionObject



177
178
179
# File 'lib/chef_core/target_host.rb', line 177

def version
  platform.release
end

#ws_cache_pathObject

Raises:

  • (NotImplementedError)


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

def ws_cache_path; raise NotImplementedError; end