chef-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "mixlib/log"
19
+
20
+ module ChefCore
21
+ class Log
22
+ extend Mixlib::Log
23
+
24
+ def self.setup(location, log_level)
25
+ if location.is_a?(String)
26
+ if location.casecmp("stdout") == 0
27
+ location = $stdout
28
+ else
29
+ location = File.open(location, "w+")
30
+ end
31
+ end
32
+ @location = location
33
+ init(location)
34
+ Log.level = log_level
35
+ end
36
+
37
+ def self.location
38
+ @location
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,63 @@
1
+
2
+
3
+ module ChefCore
4
+ class TargetHost
5
+ module Linux
6
+ def omnibus_manifest_path
7
+ # TODO - if habitat install on target, this won't work
8
+ # Note that we can't use File::Join, because that will render for the
9
+ # CURRENT platform - not the platform of the target.
10
+ "/opt/chef/version-manifest.json"
11
+ end
12
+
13
+ def mkdir(path)
14
+ run_command!("mkdir -p #{path}")
15
+ end
16
+
17
+ def chown(path, owner)
18
+ owner ||= user()
19
+ run_command!("chown #{owner} '#{path}'")
20
+ nil
21
+ end
22
+
23
+ def make_temp_dir
24
+ # We will cache this so that we only
25
+ @tempdir ||= begin
26
+ res = run_command!("bash -c '#{MKTEMP_COMMAND}'")
27
+ res.stdout.chomp.strip
28
+ end
29
+ end
30
+
31
+ def install_package(target_package_path)
32
+ install_cmd = case File.extname(target_package_path)
33
+ when ".rpm"
34
+ "rpm -Uvh #{target_package_path}"
35
+ when ".deb"
36
+ "dpkg -i #{target_package_path}"
37
+ end
38
+ run_command!(install_cmd)
39
+ nil
40
+ end
41
+
42
+ def del_file(path)
43
+ run_command!("rm -rf #{path}")
44
+ end
45
+
46
+ def del_dir(path)
47
+ del_file(path)
48
+ end
49
+
50
+ def ws_cache_path
51
+ "/var/chef-workstation"
52
+ end
53
+
54
+ # Nothing to escape in a linux-based path
55
+ def normalize_path(path)
56
+ path
57
+ end
58
+
59
+ MKTEMP_COMMAND = "d=$(mktemp -d -p${TMPDIR:-/tmp} chef_XXXXXX); echo $d".freeze
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,62 @@
1
+
2
+ module ChefCore
3
+ class TargetHost
4
+ module Windows
5
+ def omnibus_manifest_path
6
+ # TODO - use a proper method to query the win installation path -
7
+ # currently we're assuming the default, but this can be customized
8
+ # at install time.
9
+ # A working approach is below - but it runs very slowly (~10s) in testing
10
+ # on a virtualbox windows vm:
11
+ # (over winrm) Get-WmiObject Win32_Product | Where {$_.Name -match 'Chef Client'}
12
+ # TODO - if habitat install on target, this won't work
13
+ "c:\\opscode\\chef\\version-manifest.json"
14
+ end
15
+
16
+ def mkdir(path)
17
+ run_command!("New-Item -ItemType Directory -Force -Path #{path}")
18
+ end
19
+
20
+ def chown(path, owner)
21
+ # This implementation left intentionally blank.
22
+ # To date, we have not needed chown functionality on windows;
23
+ # when/if that changes we'll need to implement it here.
24
+ nil
25
+ end
26
+
27
+ def make_temp_dir
28
+ @tmpdir ||= begin
29
+ res = run_command!(MKTEMP_COMMAND)
30
+ res.stdout.chomp.strip
31
+ end
32
+ end
33
+
34
+ def install_package(target_package_path)
35
+ # While powershell does not mind the mixed path separators \ and /,
36
+ # 'cmd.exe' definitely does - so we'll make the path cmd-friendly
37
+ # before running the command
38
+ cmd = "cmd /c msiexec /package #{target_package_path.tr("/", "\\")} /quiet"
39
+ run_command!(cmd)
40
+ nil
41
+ end
42
+
43
+ def del_file(path)
44
+ run_command!("If (Test-Path #{path}) { Remove-Item -Force -Path #{path} }")
45
+ end
46
+
47
+ def del_dir(path)
48
+ run_command!("Remove-Item -Recurse -Force –Path #{path}")
49
+ end
50
+
51
+ def ws_cache_path
52
+ '#{ENV[\'APPDATA\']}/chef-workstation'
53
+ end
54
+
55
+ MKTEMP_COMMAND = "$parent = [System.IO.Path]::GetTempPath();" +
56
+ "[string] $name = [System.Guid]::NewGuid();" +
57
+ "$tmp = New-Item -ItemType Directory -Path " +
58
+ "(Join-Path $parent $name);" +
59
+ "$tmp.FullName".freeze
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,351 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017-2019 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "chef_core/log"
19
+ require "chef_core/error"
20
+ require "train"
21
+
22
+ module ChefCore
23
+ class TargetHost
24
+ attr_reader :config, :reporter, :backend, :transport_type
25
+ # These values may exist in .ssh/config but will be ignored by train
26
+ # in favor of its defaults unless we specify them explicitly.
27
+ # See #apply_ssh_config
28
+ SSH_CONFIG_OVERRIDE_KEYS = [:user, :port, :proxy].freeze
29
+
30
+ # We're borrowing a page from train here - because setting up a
31
+ # reliable connection for testing is a multi-step process,
32
+ # we'll provide this method which instantiates a TargetHost connected
33
+ # to a train mock backend. If the family/name provided resolves to a suported
34
+ # OS, this instance will mix-in the supporting methods for the given platform;
35
+ # otherwise those methods will raise NotImplementedError.
36
+ def self.mock_instance(url, family: "unknown", name: "unknown",
37
+ release: "unknown", arch: "x86_64")
38
+ # Specifying sudo: false ensures that attempted operations
39
+ # don't fail because the mock platform doesn't support sudo
40
+ target_host = TargetHost.new(url, { sudo: false })
41
+
42
+ # Don't pull in the platform-specific mixins automatically during connect
43
+ # Otherwise, it will raise since it can't resolve the OS without the mock.
44
+ target_host.instance_variable_set(:@mocked_connection, true)
45
+ target_host.connect!
46
+
47
+ # We need to provide this mock before invoking mix_in_target_platform,
48
+ # otherwise it will fail with an unknown OS (since we don't have a real connection).
49
+ target_host.backend.mock_os(
50
+ family: family,
51
+ name: name,
52
+ release: release,
53
+ arch: arch
54
+ )
55
+
56
+ # Only mix-in if we can identify the platform. This
57
+ # prevents mix_in_target_platform! from raising on unknown platform during
58
+ # tests that validate unsupported platform behaviors.
59
+ if target_host.base_os != :other
60
+ target_host.mix_in_target_platform!
61
+ end
62
+
63
+ target_host
64
+ end
65
+
66
+ def initialize(host_url, opts = {}, logger = nil)
67
+ @config = connection_config(host_url, opts, logger)
68
+ @transport_type = Train.validate_backend(@config)
69
+ apply_ssh_config(@config, opts) if @transport_type == "ssh"
70
+ @train_connection = Train.create(@transport_type, config)
71
+ end
72
+
73
+ def connection_config(host_url, opts_in, logger)
74
+ connection_opts = { target: host_url,
75
+ sudo: opts_in[:sudo] === false ? false : true,
76
+ www_form_encoded_password: true,
77
+ key_files: opts_in[:identity_file] || opts_in[:key_files],
78
+ non_interactive: true, # Prevent password prompts
79
+ connection_retries: 2,
80
+ connection_retry_sleep: 1,
81
+ logger: opts_in[:logger] || ChefCore::Log }
82
+
83
+ target_opts = Train.unpack_target_from_uri(host_url)
84
+ if opts_in.key?(:ssl) && opts_in[:ssl]
85
+ connection_opts[:ssl] = opts_in[:ssl]
86
+ connection_opts[:self_signed] = opts_in[:self_signed] || (opts_in[:ssl_verify] === false ? true : false)
87
+ end
88
+
89
+ target_opts[:host] = host_url if target_opts[:host].nil?
90
+ target_opts[:backend] = "ssh" if target_opts[:backend].nil?
91
+ connection_opts = connection_opts.merge(target_opts)
92
+
93
+ # From WinRM gem: It is recommended that you :disable_sspi => true if you are using the plaintext or ssl transport.
94
+ # See note here: https://github.com/mwrock/WinRM#example
95
+ if ["ssl", "plaintext"].include?(target_opts[:winrm_transport])
96
+ target_opts[:winrm_disable_sspi] = true
97
+ end
98
+
99
+ connection_opts = connection_opts.merge(target_opts)
100
+
101
+ # Anything we haven't explicitly set already, pass through to train.
102
+ Train.options(target_opts[:backend]).keys.each do |key|
103
+ if opts_in.key?(key) && !connection_opts.key?(key)
104
+ connection_opts[key] = opts_in[key]
105
+ end
106
+ end
107
+
108
+ Train.target_config(connection_opts)
109
+ end
110
+
111
+ def apply_ssh_config(config, opts_in)
112
+ # If we don't provide certain options, they will be defaulted
113
+ # within train - in the case of ssh, this will prevent the .ssh/config
114
+ # values from being picked up.
115
+ # Here we'll modify the returned @config to specify
116
+ # values that we get out of .ssh/config if present and if they haven't
117
+ # been explicitly given.
118
+ host_cfg = ssh_config_for_host(config[:host])
119
+ SSH_CONFIG_OVERRIDE_KEYS.each do |key|
120
+ if host_cfg.key?(key) && opts_in[key].nil?
121
+ config[key] = host_cfg[key]
122
+ end
123
+ end
124
+ end
125
+
126
+ # Establish connection to configured target.
127
+ #
128
+ def connect!
129
+ # Keep existing connections
130
+ return unless @backend.nil?
131
+ @backend = train_connection.connection
132
+ @backend.wait_until_ready
133
+
134
+ # When the testing function `mock_instance` is used, it will set
135
+ # this instance variable to false and handle this function call
136
+ # of mixin functions based on the mocked platform.
137
+ mix_in_target_platform! unless @mocked_connection
138
+ rescue Train::UserError => e
139
+ raise ConnectionFailure.new(e, config)
140
+ rescue Train::Error => e
141
+ # These are typically wrapper errors for other problems,
142
+ # so we'll prefer to use e.cause over e if available.
143
+ raise ConnectionFailure.new(e.cause || e, config)
144
+ end
145
+
146
+ def mix_in_target_platform!
147
+ case base_os
148
+ when :linux
149
+ require "chef_core/target_host/linux"
150
+ class << self; include ChefCore::TargetHost::Linux; end
151
+ when :windows
152
+ require "chef_core/target_host/windows"
153
+ class << self; include ChefCore::TargetHost::Windows; end
154
+ when :other
155
+ raise ChefCore::TargetHost::UnsupportedTargetOS.new(platform.name)
156
+ end
157
+ end
158
+
159
+ # Returns the user being used to connect. Defaults to train's default user if not specified
160
+ def user
161
+ return config[:user] unless config[:user].nil?
162
+ require "train/transports/ssh"
163
+ # TODO - this should use the right transport, not default to SSH
164
+ Train::Transports::SSH.default_options[:user][:default]
165
+ end
166
+
167
+ def hostname
168
+ config[:host]
169
+ end
170
+
171
+ def architecture
172
+ platform.arch
173
+ end
174
+
175
+ def version
176
+ platform.release
177
+ end
178
+
179
+ def base_os
180
+ if platform.windows?
181
+ :windows
182
+ elsif platform.linux?
183
+ :linux
184
+ else
185
+ :other
186
+ end
187
+ end
188
+
189
+ # TODO 2019-01-29 not expose this, it's internal implemenation. Same with #backend.
190
+ def platform
191
+ backend.platform
192
+ end
193
+
194
+ def run_command!(command, &data_handler)
195
+ result = run_command(command, &data_handler)
196
+ if result.exit_status != 0
197
+ raise RemoteExecutionFailed.new(@config[:host], command, result)
198
+ end
199
+ result
200
+ end
201
+
202
+ def run_command(command, &data_handler)
203
+ backend.run_command command, &data_handler
204
+ end
205
+
206
+ # TODO spec
207
+ def save_as_remote_file(content, remote_path)
208
+ t = Tempfile.new("chef-content")
209
+ t << content
210
+ t.close
211
+ upload_file(t.path, remote_path)
212
+ ensure
213
+ t.close
214
+ t.unlink
215
+ end
216
+
217
+ def upload_file(local_path, remote_path)
218
+ backend.upload(local_path, remote_path)
219
+ end
220
+
221
+ # Retrieve the contents of a remote file. Returns nil
222
+ # if the file didn't exist or couldn't be read.
223
+ def fetch_file_contents(remote_path)
224
+ result = backend.file(remote_path)
225
+ if result.exist? && result.file?
226
+ result.content
227
+ else
228
+ nil
229
+ end
230
+ end
231
+
232
+ # Returns the installed chef version as a Gem::Version,
233
+ # or raised ChefNotInstalled if chef client version manifest can't
234
+ # be found.
235
+ def installed_chef_version
236
+ return @installed_chef_version if @installed_chef_version
237
+ # Note: In the case of a very old version of chef (that has no manifest - pre 12.0?)
238
+ # this will report as not installed.
239
+ manifest = read_chef_version_manifest()
240
+
241
+ # We split the version here because unstable builds install from)
242
+ # are in the form "Major.Minor.Build+HASH" which is not a valid
243
+ # version string.
244
+ @installed_chef_version = Gem::Version.new(manifest["build_version"].split("+")[0])
245
+ end
246
+
247
+ def read_chef_version_manifest
248
+ manifest = fetch_file_contents(omnibus_manifest_path)
249
+ raise ChefNotInstalled.new if manifest.nil?
250
+ JSON.parse(manifest)
251
+ end
252
+
253
+ # Creates and caches location of temporary directory on the remote host
254
+ # using platform-specific implementations of make_temp_dir
255
+ # This will also set ownership to the connecting user instead of default of
256
+ # root when sudo'd, so that the dir can be used to upload files using scp
257
+ # as the connecting user.
258
+ #
259
+ # The base temp dir is cached and will only be created once per connection lifetime.
260
+ def temp_dir
261
+ dir = make_temp_dir()
262
+ chown(dir, user)
263
+ dir
264
+ end
265
+
266
+ # create a directory. because we run all commands as root, this will also set group:owner
267
+ # to the connecting user if host isn't windows so that scp -- which uses the connecting user --
268
+ # will have permissions to upload into it.
269
+ def make_directory(path)
270
+ mkdir(path)
271
+ chown(path, user)
272
+ path
273
+ end
274
+
275
+ # normalizes path across OS's
276
+ def normalize_path(p) # NOTE BOOTSTRAP: was action::base::escape_windows_path
277
+ p.tr("\\", "/")
278
+ end
279
+
280
+ # Simplified chown - just sets user, defaults to connection user. Does not touch
281
+ # group. Only has effect on non-windows targets
282
+ def chown(path, owner); raise NotImplementedError; end
283
+
284
+ # Platform-specific installation of packages
285
+ def install_package(target_package_path); raise NotImplementedError; end
286
+
287
+ def ws_cache_path; raise NotImplementedError; end
288
+
289
+ # Recursively delete directory
290
+ def del_dir(path); raise NotImplementedError; end
291
+
292
+ def del_file(path); raise NotImplementedError; end
293
+
294
+ def omnibus_manifest_path(); raise NotImplementedError; end
295
+
296
+ private
297
+
298
+ def train_connection
299
+ @train_connection
300
+ end
301
+
302
+ def ssh_config_for_host(host)
303
+ require "net/ssh"
304
+ Net::SSH::Config.for(host)
305
+ end
306
+
307
+ class RemoteExecutionFailed < ChefCore::Error
308
+ attr_reader :stdout, :stderr
309
+ def initialize(host, command, result)
310
+ super("CHEFRMT001",
311
+ command,
312
+ result.exit_status,
313
+ host,
314
+ result.stderr.empty? ? result.stdout : result.stderr)
315
+ end
316
+ end
317
+
318
+ class ConnectionFailure < ChefCore::Error
319
+ # TODO: Currently this only handles sudo-related errors;
320
+ # we should also look at e.cause for underlying connection errors
321
+ # which are presently only visible in log files.
322
+ def initialize(original_exception, connection_opts)
323
+ init_params =
324
+ # Comments below show the original_exception.reason values to check for instead of strings,
325
+ # after train 1.4.12 is consumable.
326
+ case original_exception.message # original_exception.reason
327
+ when /Sudo requires a password/ # :sudo_password_required
328
+ "CHEFTRN003"
329
+ when /Wrong sudo password/ #:bad_sudo_password
330
+ "CHEFTRN004"
331
+ when /Can't find sudo command/, /No such file/, /command not found/ # :sudo_command_not_found
332
+ # NOTE: In the /No such file/ case, reason will be nil - we still have
333
+ # to check message text. (Or PR to train to handle this case)
334
+ sudo_command = connection_opts[:sudo_command]
335
+ ["CHEFTRN005", sudo_command] # :sudo_command_not_found
336
+ when /Sudo requires a TTY.*/ # :sudo_no_tty
337
+ "CHEFTRN006"
338
+ when /has no keys added/
339
+ "CHEFTRN007"
340
+ else
341
+ ["CHEFTRN999", original_exception.message]
342
+ end
343
+ super(*(Array(init_params).flatten))
344
+ end
345
+ end
346
+ class ChefNotInstalled < StandardError; end
347
+ class UnsupportedTargetOS < ChefCore::Error
348
+ def initialize(os_name); super("CHEFTARG001", os_name); end
349
+ end
350
+ end
351
+ end