Class: ChefApply::TargetHost

Inherits:
Object
  • Object
show all
Defined in:
lib/chef_apply/target_host.rb,
lib/chef_apply/target_host/aix.rb,
lib/chef_apply/target_host/linux.rb,
lib/chef_apply/target_host/macos.rb,
lib/chef_apply/target_host/solaris.rb,
lib/chef_apply/target_host/windows.rb

Defined Under Namespace

Modules: Aix, Linux, MacOS, Solaris, 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.



65
66
67
68
69
70
# File 'lib/chef_apply/target_host.rb', line 65

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.



23
24
25
# File 'lib/chef_apply/target_host.rb', line 23

def backend
  @backend
end

#configObject (readonly)

Returns the value of attribute config.



23
24
25
# File 'lib/chef_apply/target_host.rb', line 23

def config
  @config
end

#reporterObject (readonly)

Returns the value of attribute reporter.



23
24
25
# File 'lib/chef_apply/target_host.rb', line 23

def reporter
  @reporter
end

#transport_typeObject (readonly)

Returns the value of attribute transport_type.



23
24
25
# File 'lib/chef_apply/target_host.rb', line 23

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 supported OS, this instance will mix-in the supporting methods for the given platform; otherwise those methods will raise NotImplementedError.



35
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
# File 'lib/chef_apply/target_host.rb', line 35

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



96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/chef_apply/target_host.rb', line 96

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



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

def architecture
  platform.arch
end

#base_osObject



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/chef_apply/target_host.rb', line 175

def base_os
  if platform.windows?
    :windows
  elsif platform.linux?
    :linux
  elsif platform.darwin?
    :macos
  elsif platform.solaris?
    :solaris
  elsif platform.aix?
    :aix
  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)


276
# File 'lib/chef_apply/target_host.rb', line 276

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

#connect!Object

Establish connection to configured target.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/chef_apply/target_host.rb', line 113

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
  # after the platform data is mocked; this will allow binding
  # 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



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/chef_apply/target_host.rb', line 72

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],
                      non_interactive: true,
                      # Prevent long delays due to retries on auth failure.
                      # This does reduce the number of attempts we'll make for transient conditions as well, but
                      # train does not currently exposes these as separate controls. Ideally I'd like to see a 'retry_on_auth_failure' option.
                      connection_retries: 2,
                      connection_retry_sleep: 0.15,
                      logger: ChefApply::Log }
  if opts_in.key? :ssl
    connection_opts[:ssl] = opts_in[:ssl]
    connection_opts[:self_signed] = (opts_in[:ssl_verify] === false ? true : false)
  end

  %i{sudo_password sudo sudo_command password user}.each do |key|
    connection_opts[key] = opts_in[key] if opts_in.key? key
  end

  Train.target_config(connection_opts)
end

#del_dir(path) ⇒ Object

Recursively delete directory

Raises:

  • (NotImplementedError)


284
# File 'lib/chef_apply/target_host.rb', line 284

def del_dir(path); raise NotImplementedError; end

#del_file(path) ⇒ Object

Raises:

  • (NotImplementedError)


286
# File 'lib/chef_apply/target_host.rb', line 286

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.



215
216
217
218
219
220
221
222
# File 'lib/chef_apply/target_host.rb', line 215

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

#hostnameObject



163
164
165
# File 'lib/chef_apply/target_host.rb', line 163

def hostname
  config[:host]
end

#install_package(target_package_path) ⇒ Object

Platform-specific installation of packages

Raises:

  • (NotImplementedError)


279
# File 'lib/chef_apply/target_host.rb', line 279

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.



227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/chef_apply/target_host.rb', line 227

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.



263
264
265
266
267
# File 'lib/chef_apply/target_host.rb', line 263

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

#mix_in_target_platform!Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/chef_apply/target_host.rb', line 133

def mix_in_target_platform!
  case base_os
  when :linux
    require_relative "target_host/linux"
    class << self; include ChefApply::TargetHost::Linux; end
  when :windows
    require_relative "target_host/windows"
    class << self; include ChefApply::TargetHost::Windows; end
  when :macos
    require_relative "target_host/macos"
    class << self; include ChefApply::TargetHost::MacOS; end
  when :solaris
    require_relative "target_host/solaris"
    class << self; include ChefApply::TargetHost::Solaris; end
  when :aix
    require_relative "target_host/aix"
    class << self; include ChefApply::TargetHost::Aix; end
  when :other
    raise ChefApply::TargetHost::UnsupportedTargetOS.new(platform.name)
  end
end

#normalize_path(p) ⇒ Object

normalizes path across OS’s



270
271
272
# File 'lib/chef_apply/target_host.rb', line 270

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

#omnibus_manifest_pathObject

Raises:

  • (NotImplementedError)


288
# File 'lib/chef_apply/target_host.rb', line 288

def omnibus_manifest_path(); raise NotImplementedError; end

#platformObject

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



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

def platform
  backend.platform
end

#read_chef_version_manifestObject

Raises:



240
241
242
243
244
245
# File 'lib/chef_apply/target_host.rb', line 240

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



205
206
207
# File 'lib/chef_apply/target_host.rb', line 205

def run_command(command)
  backend.run_command command
end

#run_command!(command) ⇒ Object



196
197
198
199
200
201
202
203
# File 'lib/chef_apply/target_host.rb', line 196

def run_command!(command)
  result = run_command(command)
  if result.exit_status != 0
    raise RemoteExecutionFailed.new(@config[:host], command, result)
  end

  result
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.



254
255
256
257
258
# File 'lib/chef_apply/target_host.rb', line 254

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

#upload_file(local_path, remote_path) ⇒ Object



209
210
211
# File 'lib/chef_apply/target_host.rb', line 209

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



156
157
158
159
160
161
# File 'lib/chef_apply/target_host.rb', line 156

def user
  return config[:user] unless config[:user].nil?

  require "train/transports/ssh"
  Train::Transports::SSH.default_options[:user][:default]
end

#versionObject



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

def version
  platform.release
end

#ws_cache_pathObject

Raises:

  • (NotImplementedError)


281
# File 'lib/chef_apply/target_host.rb', line 281

def ws_cache_path; raise NotImplementedError; end