chef-apply 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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