chef-apply 0.1.2

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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +26 -0
  3. data/Gemfile.lock +423 -0
  4. data/LICENSE +201 -0
  5. data/README.md +41 -0
  6. data/Rakefile +32 -0
  7. data/bin/chef-run +23 -0
  8. data/chef-apply.gemspec +67 -0
  9. data/i18n/en.yml +513 -0
  10. data/lib/chef_apply.rb +20 -0
  11. data/lib/chef_apply/action/base.rb +158 -0
  12. data/lib/chef_apply/action/converge_target.rb +173 -0
  13. data/lib/chef_apply/action/install_chef.rb +30 -0
  14. data/lib/chef_apply/action/install_chef/base.rb +137 -0
  15. data/lib/chef_apply/action/install_chef/linux.rb +38 -0
  16. data/lib/chef_apply/action/install_chef/windows.rb +54 -0
  17. data/lib/chef_apply/action/reporter.rb +39 -0
  18. data/lib/chef_apply/cli.rb +470 -0
  19. data/lib/chef_apply/cli_options.rb +145 -0
  20. data/lib/chef_apply/config.rb +150 -0
  21. data/lib/chef_apply/error.rb +108 -0
  22. data/lib/chef_apply/errors/ccr_failure_mapper.rb +93 -0
  23. data/lib/chef_apply/file_fetcher.rb +70 -0
  24. data/lib/chef_apply/log.rb +42 -0
  25. data/lib/chef_apply/recipe_lookup.rb +117 -0
  26. data/lib/chef_apply/startup.rb +162 -0
  27. data/lib/chef_apply/status_reporter.rb +42 -0
  28. data/lib/chef_apply/target_host.rb +233 -0
  29. data/lib/chef_apply/target_resolver.rb +202 -0
  30. data/lib/chef_apply/telemeter.rb +162 -0
  31. data/lib/chef_apply/telemeter/patch.rb +32 -0
  32. data/lib/chef_apply/telemeter/sender.rb +121 -0
  33. data/lib/chef_apply/temp_cookbook.rb +159 -0
  34. data/lib/chef_apply/text.rb +77 -0
  35. data/lib/chef_apply/ui/error_printer.rb +261 -0
  36. data/lib/chef_apply/ui/plain_text_element.rb +75 -0
  37. data/lib/chef_apply/ui/terminal.rb +94 -0
  38. data/lib/chef_apply/version.rb +20 -0
  39. metadata +376 -0
@@ -0,0 +1,70 @@
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 "net/http"
19
+ require "uri"
20
+ require "chef_apply/config"
21
+ require "chef_apply/log"
22
+
23
+ module ChefApply
24
+ class FileFetcher
25
+ class << self
26
+ # Simple fetcher of an http(s) url. Returns the local path
27
+ # of the downloaded file.
28
+ def fetch(path)
29
+ cache_path = ChefApply::Config.cache.path
30
+ FileUtils.mkdir_p(cache_path)
31
+ url = URI.parse(path)
32
+ name = File.basename(url.path)
33
+ local_path = File.join(cache_path, name)
34
+
35
+ # TODO header check for size or checksum?
36
+ return local_path if File.exist?(local_path)
37
+
38
+ download_file(url, local_path)
39
+ local_path
40
+ end
41
+
42
+ def download_file(url, local_path)
43
+ temp_path = "#{local_path}.downloading"
44
+ file = open(temp_path, "wb")
45
+ ChefApply::Log.debug "Downloading: #{temp_path}"
46
+ Net::HTTP.start(url.host) do |http|
47
+ begin
48
+ http.request_get(url.path) do |resp|
49
+ resp.read_body do |segment|
50
+ file.write(segment)
51
+ end
52
+ end
53
+ rescue e
54
+ @error = true
55
+ raise
56
+ ensure
57
+ file.close()
58
+ # If any failures occurred, don't risk keeping
59
+ # an incomplete download that we'll see as 'cached'
60
+ if @error
61
+ FileUtils.rm_f(temp_path)
62
+ else
63
+ FileUtils.mv(temp_path, local_path)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -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 ChefApply
21
+ class Log
22
+ extend Mixlib::Log
23
+
24
+ def self.setup(location, log_level)
25
+ @location = location
26
+ if location.is_a?(String)
27
+ if location.casecmp("stdout") == 0
28
+ location = $stdout
29
+ else
30
+ location = File.open(location, "w+")
31
+ end
32
+ end
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,117 @@
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 "chef-config/config"
19
+ require "chef_apply/config"
20
+ require "chef_apply/error"
21
+ require "chef_apply/log"
22
+
23
+ module ChefApply
24
+ # When users are trying to converge a local recipe on a remote target, there
25
+ # is a very specific (but expansive) set of things they can specify. This
26
+ # class encapsulates that logic for testing purposes. We either return
27
+ # a path to a recipe or we raise an error.
28
+ class RecipeLookup
29
+
30
+ attr_reader :cookbook_repo_paths
31
+ def initialize(cookbook_repo_paths)
32
+ @cookbook_repo_paths = cookbook_repo_paths
33
+ end
34
+
35
+ # The recipe specifier is provided by the customer as either a path OR
36
+ # a cookbook and optional recipe name.
37
+ def split(recipe_specifier)
38
+ recipe_specifier.split("::")
39
+ end
40
+
41
+ # Given a cookbook path or name, try to load that cookbook. Either return
42
+ # a cookbook object or raise an error.
43
+ def load_cookbook(path_or_name)
44
+ require "chef/exceptions"
45
+ if File.directory?(path_or_name)
46
+ cookbook_path = path_or_name
47
+ # First, is there a cookbook in the specified dir that matches?
48
+ require "chef/cookbook/cookbook_version_loader"
49
+ begin
50
+ v = Chef::Cookbook::CookbookVersionLoader.new(cookbook_path)
51
+ v.load!
52
+ cookbook = v.cookbook_version
53
+ rescue Chef::Exceptions::CookbookNotFoundInRepo
54
+ raise InvalidCookbook.new(cookbook_path)
55
+ end
56
+ else
57
+ cookbook_name = path_or_name
58
+ # Second, is there a cookbook in their local repository that matches?
59
+ require "chef/cookbook_loader"
60
+ cb_loader = Chef::CookbookLoader.new(cookbook_repo_paths)
61
+ cb_loader.load_cookbooks_without_shadow_warning
62
+
63
+ begin
64
+ cookbook = cb_loader[cookbook_name]
65
+ rescue Chef::Exceptions::CookbookNotFoundInRepo
66
+ cookbook_repo_paths.each do |repo_path|
67
+ cookbook_path = File.join(repo_path, cookbook_name)
68
+ if File.directory?(cookbook_path)
69
+ raise InvalidCookbook.new(cookbook_path)
70
+ end
71
+ end
72
+ raise CookbookNotFound.new(cookbook_name, cookbook_repo_paths)
73
+ end
74
+ end
75
+ cookbook
76
+ end
77
+
78
+ # Find the specified recipe or default recipe if none is specified.
79
+ # Raise an error if recipe cannot be found.
80
+ def find_recipe(cookbook, recipe_name = nil)
81
+ recipes = cookbook.recipe_filenames_by_name
82
+ if recipe_name.nil?
83
+ default_recipe = recipes["default"]
84
+ raise NoDefaultRecipe.new(cookbook.root_dir, cookbook.name) if default_recipe.nil?
85
+ default_recipe
86
+ else
87
+ recipe = recipes[recipe_name]
88
+ raise RecipeNotFound.new(cookbook.root_dir, recipe_name, recipes.keys, cookbook.name) if recipe.nil?
89
+ recipe
90
+ end
91
+ end
92
+
93
+ class InvalidCookbook < ChefApply::Error
94
+ def initialize(cookbook_path); super("CHEFVAL005", cookbook_path); end
95
+ end
96
+
97
+ class CookbookNotFound < ChefApply::Error
98
+ def initialize(cookbook_name, repo_paths)
99
+ repo_paths = repo_paths.join("\n")
100
+ super("CHEFVAL006", cookbook_name, repo_paths)
101
+ end
102
+ end
103
+
104
+ class NoDefaultRecipe < ChefApply::Error
105
+ def initialize(cookbook_path, cookbook_name); super("CHEFVAL007", cookbook_path, cookbook_name); end
106
+ end
107
+
108
+ class RecipeNotFound < ChefApply::Error
109
+ def initialize(cookbook_path, recipe_name, available_recipes, cookbook_name)
110
+ available_recipes.map! { |r| "'#{r}'" }
111
+ available_recipes = available_recipes.join(", ")
112
+ super("CHEFVAL008", cookbook_path, recipe_name, available_recipes, cookbook_name)
113
+ end
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,162 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 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
+ require "chef_apply/config"
18
+ require "chef_apply/text"
19
+ require "chef_apply/ui/terminal"
20
+ require "chef_apply/telemeter/sender"
21
+ module ChefApply
22
+ class Startup
23
+ attr_reader :argv
24
+ T = ChefApply::Text.cli
25
+
26
+ def initialize(argv)
27
+ @term_init = false
28
+ @argv = argv.clone
29
+ # Enable CLI output via Terminal. This comes first because other startup steps may
30
+ # need to output to the terminal.
31
+ init_terminal
32
+ end
33
+
34
+ def run
35
+ # Some tasks we do only once in an installation:
36
+ first_run_tasks
37
+
38
+ # Call this every time, so that if we add or change ~/.chef-workstation
39
+ # directory structure, we can be sure that it exists. Even with a
40
+ # custom configuration, the .chef-workstation directory and subdirs
41
+ # are required.
42
+ setup_workstation_user_directories
43
+
44
+ # Startup tasks that may change behavior based on configuration value
45
+ # must be run after load_config
46
+ load_config
47
+
48
+ # Init logging using log level out of config
49
+ setup_logging
50
+
51
+ # Begin upload of previous session telemetry. (If telemetry is not enabled,
52
+ # in config the uploader will clean up previous session(s) without sending)
53
+ start_telemeter_upload
54
+
55
+ # Launch the actual Chef Apply behavior
56
+ start_chef_apply
57
+
58
+ # NOTE: Because these exceptions occur outside of the
59
+ # CLI handling, they won't be tracked in telemtry.
60
+ # We can revisit this once the pending error handling rework
61
+ # is underway.
62
+ rescue ConfigPathInvalid => e
63
+ UI::Terminal.output(T.error.bad_config_file(e.path))
64
+ rescue ConfigPathNotProvided
65
+ UI::Terminal.output(T.error.missing_config_path)
66
+ rescue Mixlib::Config::UnknownConfigOptionError => e
67
+ # Ideally we'd update the exception in mixlib to include
68
+ # a field with the faulty value, line number, and nested context -
69
+ # it's less fragile than depending on text parsing, which
70
+ # is what we'll do for now.
71
+ if e.message =~ /.*unsupported config value (.*)[.]+$/
72
+ # TODO - levenshteinian distance to figure out
73
+ # what they may have meant instead.
74
+ UI::Terminal.output(T.error.invalid_config_key($1, Config.location))
75
+ else
76
+ # Safety net in case the error text changes from under us.
77
+ UI::Terminal.output(T.error.unknown_config_error(e.message, Config.location))
78
+ end
79
+ rescue Tomlrb::ParseError => e
80
+ UI::Terminal.output(T.error.unknown_config_error(e.message, Config.location))
81
+ end
82
+
83
+ def init_terminal
84
+ UI::Terminal.init($stdout)
85
+ end
86
+
87
+ def first_run_tasks
88
+ return if Dir.exist?(Config::WS_BASE_PATH)
89
+ create_default_config
90
+ setup_telemetry
91
+ end
92
+
93
+ def create_default_config
94
+ UI::Terminal.output T.creating_config(Config.default_location)
95
+ UI::Terminal.output ""
96
+ FileUtils.mkdir_p(Config::WS_BASE_PATH)
97
+ FileUtils.touch(Config.default_location)
98
+ end
99
+
100
+ def setup_telemetry
101
+ require "securerandom"
102
+ installation_id = SecureRandom.uuid
103
+ File.write(Config.telemetry_installation_identifier_file, installation_id)
104
+
105
+ # Tell the user we're anonymously tracking, give brief opt-out
106
+ # and a link to detailed information.
107
+ UI::Terminal.output T.telemetry_enabled(Config.location)
108
+ UI::Terminal.output ""
109
+ end
110
+
111
+ def start_telemeter_upload
112
+ ChefApply::Telemeter::Sender.start_upload_thread()
113
+ end
114
+
115
+ def setup_workstation_user_directories
116
+ # Note that none of these paths are customizable in config, so
117
+ # it's safe to do before we load config.
118
+ FileUtils.mkdir_p(Config::WS_BASE_PATH)
119
+ FileUtils.mkdir_p(Config.base_log_directory)
120
+ FileUtils.mkdir_p(Config.telemetry_path)
121
+ end
122
+
123
+ def load_config
124
+ path = custom_config_path
125
+ Config.custom_location(path) unless path.nil?
126
+ Config.load
127
+ end
128
+
129
+ # Look for a user-supplied config path by manually parsing the option.
130
+ # Note that we can't use Mixlib::CLI for this.
131
+ # To ensure that ChefApply::CLI initializes with correct
132
+ # option defaults, we need to have configuraton loaded before initializing it.
133
+ def custom_config_path
134
+ argv.each_with_index do |arg, index|
135
+ if arg == "--config-path" || arg == "-c"
136
+ next_arg = argv[index + 1]
137
+ raise ConfigPathNotProvided.new if next_arg.nil?
138
+ raise ConfigPathInvalid.new(next_arg) unless File.file?(next_arg) && File.readable?(next_arg)
139
+ return next_arg
140
+ end
141
+ end
142
+ nil
143
+ end
144
+
145
+ def setup_logging
146
+ ChefApply::Log.setup(Config.log.location, Config.log.level.to_sym)
147
+ ChefApply::Log.info("Initialized logger")
148
+ end
149
+
150
+ def start_chef_apply
151
+ require "chef_apply/cli"
152
+ ChefApply::CLI.new(@argv).run
153
+ end
154
+ class ConfigPathNotProvided < StandardError; end
155
+ class ConfigPathInvalid < StandardError
156
+ attr_reader :path
157
+ def initialize(path)
158
+ @path = path
159
+ end
160
+ end
161
+ end
162
+ end
@@ -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
+ module ChefApply
19
+ class StatusReporter
20
+
21
+ def initialize(ui_element, prefix: nil, key: nil)
22
+ @ui_element = ui_element
23
+ @key = key
24
+ @ui_element.update(prefix: prefix)
25
+ end
26
+
27
+ def update(msg)
28
+ @ui_element.update({ @key => msg })
29
+ end
30
+
31
+ def success(msg)
32
+ update(msg)
33
+ @ui_element.success
34
+ end
35
+
36
+ def error(msg)
37
+ update(msg)
38
+ @ui_element.error
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,233 @@
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 "chef_apply/log"
19
+ require "chef_apply/error"
20
+ require "train"
21
+ module ChefApply
22
+ class TargetHost
23
+ attr_reader :config, :reporter, :backend, :transport_type
24
+ # These values may exist in .ssh/config but will be ignored by train
25
+ # in favor of its defaults unless we specify them explicitly.
26
+ # See #apply_ssh_config
27
+ SSH_CONFIG_OVERRIDE_KEYS = [:user, :port, :proxy]
28
+
29
+ def self.instance_for_url(target, opts = {})
30
+ opts = { target: @url }
31
+ target_host = new(target, opts)
32
+ target_host.connect!
33
+ target_host
34
+ end
35
+
36
+ def initialize(host_url, opts = {}, logger = nil)
37
+ @config = connection_config(host_url, opts, logger)
38
+ @transport_type = Train.validate_backend(@config)
39
+ apply_ssh_config(@config, opts) if @transport_type == "ssh"
40
+ @train_connection = Train.create(@transport_type, config)
41
+ end
42
+
43
+ def connection_config(host_url, opts_in, logger)
44
+ connection_opts = { target: host_url,
45
+ sudo: opts_in[:sudo] === false ? false : true,
46
+ www_form_encoded_password: true,
47
+ key_files: opts_in[:identity_file],
48
+ logger: ChefApply::Log }
49
+ if opts_in.has_key? :ssl
50
+ connection_opts[:ssl] = opts_in[:ssl]
51
+ connection_opts[:self_signed] = (opts_in[:ssl_verify] === false ? true : false)
52
+ end
53
+
54
+ [:sudo_password, :sudo, :sudo_command, :password, :user].each do |key|
55
+ connection_opts[key] = opts_in[key] if opts_in.has_key? key
56
+ end
57
+
58
+ Train.target_config(connection_opts)
59
+ end
60
+
61
+ def apply_ssh_config(config, opts_in)
62
+ # If we don't provide certain options, they will be defaulted
63
+ # within train - in the case of ssh, this will prevent the .ssh/config
64
+ # values from being picked up.
65
+ # Here we'll modify the returned @config to specify
66
+ # values that we get out of .ssh/config if present and if they haven't
67
+ # been explicitly given.
68
+ host_cfg = ssh_config_for_host(config[:host])
69
+ SSH_CONFIG_OVERRIDE_KEYS.each do |key|
70
+ if host_cfg.has_key?(key) && opts_in[key].nil?
71
+ config[key] = host_cfg[key]
72
+ end
73
+ end
74
+ end
75
+
76
+ def connect!
77
+ return unless @backend.nil?
78
+ @backend = train_connection.connection
79
+ @backend.wait_until_ready
80
+ rescue Train::UserError => e
81
+ raise ConnectionFailure.new(e, config)
82
+ rescue Train::Error => e
83
+ # These are typically wrapper errors for other problems,
84
+ # so we'll prefer to use e.cause over e if available.
85
+ raise ConnectionFailure.new(e.cause || e, config)
86
+ end
87
+
88
+ # Returns the user being used to connect. Defaults to train's default user if not specified
89
+ # defaulted in .ssh/config (for ssh connections), as set up in '#apply_ssh_config'.
90
+ def user
91
+ return config[:user] unless config[:user].nil?
92
+ require "train/transports/ssh"
93
+ Train::Transports::SSH.default_options[:user][:default]
94
+ end
95
+
96
+ def hostname
97
+ config[:host]
98
+ end
99
+
100
+ def architecture
101
+ platform.arch
102
+ end
103
+
104
+ def version
105
+ platform.release
106
+ end
107
+
108
+ def base_os
109
+ if platform.family == "windows"
110
+ :windows
111
+ elsif platform.linux?
112
+ :linux
113
+ else
114
+ # TODO - this seems like it shouldn't happen here, when
115
+ # all the caller is doing is asking about the OS
116
+ raise ChefApply::TargetHost::UnsupportedTargetOS.new(platform.name)
117
+ end
118
+ end
119
+
120
+ def platform
121
+ backend.platform
122
+ end
123
+
124
+ def run_command!(command, sudo_as_user = false)
125
+ result = run_command(command, sudo_as_user)
126
+ if result.exit_status != 0
127
+ raise RemoteExecutionFailed.new(@config[:host], command, result)
128
+ end
129
+ result
130
+ end
131
+
132
+ def run_command(command, sudo_as_user = false)
133
+ if config[:sudo] && sudo_as_user && base_os == :linux
134
+ command = "-u #{config[:user]} #{command}"
135
+ end
136
+ backend.run_command command
137
+ end
138
+
139
+ def upload_file(local_path, remote_path)
140
+ backend.upload(local_path, remote_path)
141
+ end
142
+
143
+ # Returns the installed chef version as a Gem::Version,
144
+ # or raised ChefNotInstalled if chef client version manifest can't
145
+ # be found.
146
+ def installed_chef_version
147
+ return @installed_chef_version if @installed_chef_version
148
+ # Note: In the case of a very old version of chef (that has no manifest - pre 12.0?)
149
+ # this will report as not installed.
150
+ manifest = get_chef_version_manifest()
151
+ raise ChefNotInstalled.new if manifest == :not_found
152
+ # We'll split the version here because unstable builds (where we currently
153
+ # install from) are in the form "Major.Minor.Build+HASH" which is not a valid
154
+ # version string.
155
+ @installed_chef_version = Gem::Version.new(manifest["build_version"].split("+")[0])
156
+ end
157
+
158
+ MANIFEST_PATHS = {
159
+ # TODO - use a proper method to query the win installation path -
160
+ # currently we're assuming the default, but this can be customized
161
+ # at install time.
162
+ # A working approach is below - but it runs very slowly in testing
163
+ # on a virtualbox windows vm:
164
+ # (over winrm) Get-WmiObject Win32_Product | Where {$_.Name -match 'Chef Client'}
165
+ windows: "c:\\opscode\\chef\\version-manifest.json",
166
+ linux: "/opt/chef/version-manifest.json"
167
+ }
168
+
169
+ def get_chef_version_manifest
170
+ path = MANIFEST_PATHS[base_os()]
171
+ manifest = backend.file(path)
172
+ return :not_found unless manifest.file?
173
+ JSON.parse(manifest.content)
174
+ end
175
+
176
+ private
177
+
178
+ def train_connection
179
+ @train_connection
180
+ end
181
+
182
+ def ssh_config_for_host(host)
183
+ require "net/ssh"
184
+ Net::SSH::Config.for(host)
185
+ end
186
+
187
+ class RemoteExecutionFailed < ChefApply::ErrorNoLogs
188
+ attr_reader :stdout, :stderr
189
+ def initialize(host, command, result)
190
+ super("CHEFRMT001",
191
+ command,
192
+ result.exit_status,
193
+ host,
194
+ result.stderr.empty? ? result.stdout : result.stderr)
195
+ end
196
+ end
197
+
198
+ class ConnectionFailure < ChefApply::ErrorNoLogs
199
+ # TODO: Currently this only handles sudo-related errors;
200
+ # we should also look at e.cause for underlying connection errors
201
+ # which are presently only visible in log files.
202
+ def initialize(original_exception, connection_opts)
203
+ sudo_command = connection_opts[:sudo_command]
204
+ init_params =
205
+ # Comments below show the original_exception.reason values to check for instead of strings,
206
+ # after train 1.4.12 is consumable.
207
+ case original_exception.message # original_exception.reason
208
+ when /Sudo requires a password/ # :sudo_password_required
209
+ "CHEFTRN003"
210
+ when /Wrong sudo password/ #:bad_sudo_password
211
+ "CHEFTRN004"
212
+ when /Can't find sudo command/, /No such file/, /command not found/ # :sudo_command_not_found
213
+ # NOTE: In the /No such file/ case, reason will be nil - we still have
214
+ # to check message text. (Or PR to train to handle this case)
215
+ ["CHEFTRN005", sudo_command] # :sudo_command_not_found
216
+ when /Sudo requires a TTY.*/ # :sudo_no_tty
217
+ "CHEFTRN006"
218
+ when /has no keys added/
219
+ "CHEFTRN007"
220
+ else
221
+ ["CHEFTRN999", original_exception.message]
222
+ end
223
+ super(*(Array(init_params).flatten))
224
+ end
225
+ end
226
+
227
+ class ChefNotInstalled < StandardError; end
228
+
229
+ class UnsupportedTargetOS < ChefApply::ErrorNoLogs
230
+ def initialize(os_name); super("CHEFTARG001", os_name); end
231
+ end
232
+ end
233
+ end