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,38 @@
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::Action::InstallChef
19
+ class Linux < ChefApply::Action::InstallChef::Base
20
+ def install_chef_to_target(remote_path)
21
+ install_cmd = case File.extname(remote_path)
22
+ when ".rpm"
23
+ "rpm -Uvh #{remote_path}"
24
+ when ".deb"
25
+ "dpkg -i #{remote_path}"
26
+ end
27
+ target_host.run_command!(install_cmd)
28
+ nil
29
+ end
30
+
31
+ def setup_remote_temp_path
32
+ installer_dir = "/tmp/chef-installer"
33
+ target_host.run_command!("mkdir -p #{installer_dir}")
34
+ target_host.run_command!("chmod 777 #{installer_dir}")
35
+ installer_dir
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,54 @@
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::Action::InstallChef
19
+ class Windows < ChefApply::Action::InstallChef::Base
20
+
21
+ def perform_remote_install
22
+ require "mixlib/install"
23
+ installer = Mixlib::Install.new({
24
+ platform: "windows",
25
+ product_name: "chef",
26
+ channel: :stable,
27
+ shell_type: :ps1,
28
+ version: "13"
29
+ })
30
+ target_host.run_command! installer.install_command
31
+ end
32
+
33
+ # TODO: These methods are implemented, but are currently
34
+ # not runnable - see explanation in InstallChef::Base
35
+ def install_chef_to_target(remote_path)
36
+ # While powershell does not mind the mixed path separators \ and /,
37
+ # 'cmd.exe' definitely does - so we'll make the path cmd-friendly
38
+ # before running the command
39
+ cmd = "cmd /c msiexec /package #{remote_path.tr("/", "\\")} /quiet"
40
+ target_host.run_command!(cmd)
41
+ end
42
+
43
+ def setup_remote_temp_path
44
+ return @temppath if @temppath
45
+
46
+ r = target_host.run_command!("Write-Host -NoNewline $env:TEMP")
47
+ temppath = "#{r.stdout}\\chef-installer"
48
+
49
+ # Failure here is acceptable - the dir could already exist
50
+ target_host.run_command("New-Item -ItemType Directory -Force -Path #{temppath}")
51
+ @temppath = temppath
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
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/handler"
19
+ require "chef/resource/directory"
20
+
21
+ module ChefApply
22
+ class Reporter < ::Chef::Handler
23
+
24
+ def report
25
+ if exception
26
+ Chef::Log.error("Creating exception report")
27
+ else
28
+ Chef::Log.info("Creating run report")
29
+ end
30
+
31
+ #ensure start time and end time are output in the json properly in the event activesupport happens to be on the system
32
+ run_data = data
33
+ run_data[:start_time] = run_data[:start_time].to_s
34
+ run_data[:end_time] = run_data[:end_time].to_s
35
+
36
+ Chef::FileCache.store("run-report.json", Chef::JSONCompat.to_json_pretty(run_data), 0640)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,470 @@
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 "mixlib/cli"
18
+ require "chef/log"
19
+ require "chef-config/config"
20
+ require "chef-config/logger"
21
+
22
+ require "chef_apply/cli_options"
23
+ require "chef_apply/action/converge_target"
24
+ require "chef_apply/action/install_chef"
25
+ require "chef_apply/config"
26
+ require "chef_apply/error"
27
+ require "chef_apply/log"
28
+ require "chef_apply/recipe_lookup"
29
+ require "chef_apply/target_host"
30
+ require "chef_apply/target_resolver"
31
+ require "chef_apply/telemeter"
32
+ require "chef_apply/telemeter/sender"
33
+ require "chef_apply/temp_cookbook"
34
+ require "chef_apply/ui/terminal"
35
+ require "chef_apply/ui/error_printer"
36
+ require "chef_apply/version"
37
+
38
+ module ChefApply
39
+ class CLI
40
+ include Mixlib::CLI
41
+ include ChefApply::CLIOptions
42
+
43
+ RC_OK = 0
44
+ RC_COMMAND_FAILED = 1
45
+ RC_UNHANDLED_ERROR = 32
46
+ RC_ERROR_HANDLING_FAILED = 64
47
+
48
+ def initialize(argv)
49
+ @argv = argv.clone
50
+ @rc = RC_OK
51
+ super()
52
+ end
53
+
54
+ def run
55
+ # Perform a timing and capture of the run. Individual methods and actions may perform
56
+ # nested Telemeter.timed_*_capture or Telemeter.capture calls in their operation, and
57
+ # they will be captured in the same telemetry session.
58
+ # NOTE: We're not currently sending arguments to telemetry because we have not implemented
59
+ # pre-parsing of arguments to eliminate potentially sensitive data such as
60
+ # passwords in host name, or in ad-hoc converge properties.
61
+ Telemeter.timed_run_capture([:redacted]) do
62
+ begin
63
+ perform_run
64
+ rescue Exception => e
65
+ @rc = handle_run_error(e)
66
+ end
67
+ end
68
+ rescue => e
69
+ @rc = handle_run_error(e)
70
+ ensure
71
+ Telemeter.commit
72
+ exit @rc
73
+ end
74
+
75
+ def handle_run_error(e)
76
+ case e
77
+ when nil
78
+ RC_OK
79
+ when WrappedError
80
+ UI::ErrorPrinter.show_error(e)
81
+ RC_COMMAND_FAILED
82
+ when SystemExit
83
+ e.status
84
+ when Exception
85
+ UI::ErrorPrinter.dump_unexpected_error(e)
86
+ RC_ERROR_HANDLING_FAILED
87
+ else
88
+ UI::ErrorPrinter.dump_unexpected_error(e)
89
+ RC_UNHANDLED_ERROR
90
+ end
91
+ end
92
+
93
+ def perform_run
94
+ parse_options(@argv)
95
+ if @argv.empty? || parsed_options[:help]
96
+ show_help
97
+ elsif parsed_options[:version]
98
+ show_version
99
+ else
100
+ validate_params(cli_arguments)
101
+ configure_chef
102
+ target_hosts = TargetResolver.new(cli_arguments.shift,
103
+ parsed_options.delete(:protocol),
104
+ parsed_options).targets
105
+ temp_cookbook, initial_status_msg = generate_temp_cookbook(cli_arguments)
106
+ local_policy_path = nil
107
+ UI::Terminal.render_job(TS.generate_policyfile.generating) do |reporter|
108
+ local_policy_path = create_local_policy(temp_cookbook)
109
+ reporter.success(TS.generate_policyfile.success)
110
+ end
111
+ if target_hosts.length == 1
112
+ # Note: UX discussed determined that when running with a single target,
113
+ # we'll use multiple lines to display status for the target.
114
+ run_single_target(initial_status_msg, target_hosts[0], local_policy_path)
115
+ else
116
+ @multi_target = true
117
+ # Multi-target will use one line per target.
118
+ run_multi_target(initial_status_msg, target_hosts, local_policy_path)
119
+ end
120
+ end
121
+ rescue OptionParser::InvalidOption => e
122
+ # Using nil here is a bit gross but it prevents usage from printing.
123
+ ove = OptionValidationError.new("CHEFVAL010", nil,
124
+ e.message.split(":")[1].strip, # only want the flag
125
+ format_flags.lines[1..-1].join # remove 'FLAGS:' header
126
+ )
127
+ handle_perform_error(ove)
128
+ rescue => e
129
+ handle_perform_error(e)
130
+ ensure
131
+ temp_cookbook.delete unless temp_cookbook.nil?
132
+ end
133
+
134
+ # Accepts a target_host and establishes the connection to that host
135
+ # while providing visual feedback via the Terminal API.
136
+ def connect_target(target_host, reporter = nil)
137
+ connect_message = T.status.connecting(target_host.user)
138
+ if reporter.nil?
139
+ UI::Terminal.render_job(connect_message, prefix: "[#{target_host.config[:host]}]") do |rep|
140
+ do_connect(target_host, rep, :success)
141
+ end
142
+ else
143
+ reporter.update(connect_message)
144
+ do_connect(target_host, reporter, :update)
145
+ end
146
+ target_host
147
+ end
148
+
149
+ def run_single_target(initial_status_msg, target_host, local_policy_path)
150
+ connect_target(target_host)
151
+ prefix = "[#{target_host.hostname}]"
152
+ UI::Terminal.render_job(TS.install_chef.verifying, prefix: prefix) do |reporter|
153
+ install(target_host, reporter)
154
+ end
155
+ UI::Terminal.render_job(initial_status_msg, prefix: "[#{target_host.hostname}]") do |reporter|
156
+ converge(reporter, local_policy_path, target_host)
157
+ end
158
+ end
159
+
160
+ def run_multi_target(initial_status_msg, target_hosts, local_policy_path)
161
+ # Our multi-host UX does not show a line item per action,
162
+ # but rather a line-item per connection.
163
+ jobs = target_hosts.map do |target_host|
164
+ # This block will run in its own thread during render.
165
+ UI::Terminal::Job.new("[#{target_host.hostname}]", target_host) do |reporter|
166
+ connect_target(target_host, reporter)
167
+ reporter.update(TS.install_chef.verifying)
168
+ install(target_host, reporter)
169
+ reporter.update(initial_status_msg)
170
+ converge(reporter, local_policy_path, target_host)
171
+ end
172
+ end
173
+ UI::Terminal.render_parallel_jobs(TS.converge.multi_header, jobs)
174
+ handle_job_failures(jobs)
175
+ end
176
+
177
+ # The first param is always hostname. Then we either have
178
+ # 1. A recipe designation
179
+ # 2. A resource type and resource name followed by any properties
180
+ PROPERTY_MATCHER = /^([a-zA-Z0-9_]+)=(.+)$/
181
+ CB_MATCHER = '[\w\-]+'
182
+ def validate_params(params)
183
+ if params.size < 2
184
+ raise OptionValidationError.new("CHEFVAL002", self)
185
+ end
186
+ if params.size == 2
187
+ # Trying to specify a recipe to run remotely, no properties
188
+ cb = params[1]
189
+ if File.exist?(cb)
190
+ # This is a path specification, and we know it is valid
191
+ elsif cb =~ /^#{CB_MATCHER}$/ || cb =~ /^#{CB_MATCHER}::#{CB_MATCHER}$/
192
+ # They are specifying a cookbook as 'cb_name' or 'cb_name::recipe'
193
+ else
194
+ raise OptionValidationError.new("CHEFVAL004", self, cb)
195
+ end
196
+ elsif params.size >= 3
197
+ properties = params[3..-1]
198
+ properties.each do |property|
199
+ unless property =~ PROPERTY_MATCHER
200
+ raise OptionValidationError.new("CHEFVAL003", self, property)
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ # Now that we are leveraging Chef locally we want to perform some initial setup of it
207
+ def configure_chef
208
+ ChefConfig.logger = ChefApply::Log
209
+ # Setting the config isn't enough, we need to ensure the logger is initialized
210
+ # or automatic initialization will still go to stdout
211
+ Chef::Log.init(ChefApply::Log)
212
+ Chef::Log.level = ChefApply::Log.level
213
+ end
214
+
215
+ def format_properties(string_props)
216
+ properties = {}
217
+ string_props.each do |a|
218
+ key, value = PROPERTY_MATCHER.match(a)[1..-1]
219
+ value = transform_property_value(value)
220
+ properties[key] = value
221
+ end
222
+ properties
223
+ end
224
+
225
+ # Incoming properties are always read as a string from the command line.
226
+ # Depending on their type we should transform them so we do not try and pass
227
+ # a string to a resource property that expects an integer or boolean.
228
+ def transform_property_value(value)
229
+ case value
230
+ when /^0/
231
+ # when it is a zero leading value like "0777" don't turn
232
+ # it into a number (this is a mode flag)
233
+ value
234
+ when /^\d+$/
235
+ value.to_i
236
+ when /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/
237
+ value.to_f
238
+ when /true/i
239
+ true
240
+ when /false/i
241
+ false
242
+ else
243
+ value
244
+ end
245
+ end
246
+
247
+ # The user will either specify a single resource on the command line, or a recipe.
248
+ # We need to parse out those two different situations
249
+ def generate_temp_cookbook(cli_arguments)
250
+ temp_cookbook = TempCookbook.new
251
+ if recipe_strategy?(cli_arguments)
252
+ recipe_specifier = cli_arguments.shift
253
+ ChefApply::Log.debug("Beginning to look for recipe specified as #{recipe_specifier}")
254
+ if File.file?(recipe_specifier)
255
+ ChefApply::Log.debug("#{recipe_specifier} is a valid path to a recipe")
256
+ recipe_path = recipe_specifier
257
+ else
258
+ rl = RecipeLookup.new(parsed_options[:cookbook_repo_paths])
259
+ cookbook_path_or_name, optional_recipe_name = rl.split(recipe_specifier)
260
+ cookbook = rl.load_cookbook(cookbook_path_or_name)
261
+ recipe_path = rl.find_recipe(cookbook, optional_recipe_name)
262
+ end
263
+ temp_cookbook.from_existing_recipe(recipe_path)
264
+ initial_status_msg = TS.converge.converging_recipe(recipe_specifier)
265
+ else
266
+ resource_type = cli_arguments.shift
267
+ resource_name = cli_arguments.shift
268
+ temp_cookbook.from_resource(resource_type, resource_name, format_properties(cli_arguments))
269
+ full_rs_name = "#{resource_type}[#{resource_name}]"
270
+ ChefApply::Log.debug("Converging resource #{full_rs_name} on target")
271
+ initial_status_msg = TS.converge.converging_resource(full_rs_name)
272
+ end
273
+
274
+ [temp_cookbook, initial_status_msg]
275
+ end
276
+
277
+ def recipe_strategy?(cli_arguments)
278
+ cli_arguments.size == 1
279
+ end
280
+
281
+ def create_local_policy(local_cookbook)
282
+ require "chef-dk/ui"
283
+ require "chef-dk/policyfile_services/export_repo"
284
+ require "chef-dk/policyfile_services/install"
285
+ policyfile_installer = ChefDK::PolicyfileServices::Install.new(
286
+ ui: ChefDK::UI.null(),
287
+ root_dir: local_cookbook.path
288
+ )
289
+ begin
290
+ policyfile_installer.run
291
+ rescue ChefDK::PolicyfileInstallError => e
292
+ raise PolicyfileInstallError.new(e)
293
+ end
294
+ lock_path = File.join(local_cookbook.path, "Policyfile.lock.json")
295
+ es = ChefDK::PolicyfileServices::ExportRepo.new(policyfile: lock_path,
296
+ root_dir: local_cookbook.path,
297
+ export_dir: File.join(local_cookbook.path, "export"),
298
+ archive: true,
299
+ force: true)
300
+ es.run
301
+ es.archive_file_location
302
+ end
303
+
304
+ # Runs the InstallChef action and renders UI updates as
305
+ # the action reports back
306
+ def install(target_host, reporter)
307
+ installer = Action::InstallChef.instance_for_target(target_host, check_only: !parsed_options[:install])
308
+ context = TS.install_chef
309
+ installer.run do |event, data|
310
+ case event
311
+ when :installing
312
+ if installer.upgrading?
313
+ message = context.upgrading(target_host.installed_chef_version, installer.version_to_install)
314
+ else
315
+ message = context.installing(installer.version_to_install)
316
+ end
317
+ reporter.update(message)
318
+ when :uploading
319
+ reporter.update(context.uploading)
320
+ when :downloading
321
+ reporter.update(context.downloading)
322
+ when :already_installed
323
+ meth = @multi_target ? :update : :success
324
+ reporter.send(meth, context.already_present(target_host.installed_chef_version))
325
+ when :install_complete
326
+ meth = @multi_target ? :update : :success
327
+ if installer.upgrading?
328
+ message = context.upgrade_success(target_host.installed_chef_version, installer.version_to_install)
329
+ else
330
+ message = context.install_success(installer.version_to_install)
331
+ end
332
+ reporter.send(meth, message)
333
+ else
334
+ handle_message(event, data, reporter)
335
+ end
336
+ end
337
+ end
338
+
339
+ # Runs the Converge action and renders UI updates as
340
+ # the action reports back
341
+ def converge(reporter, local_policy_path, target_host)
342
+ converge_args = { local_policy_path: local_policy_path, target_host: target_host }
343
+ converger = Action::ConvergeTarget.new(converge_args)
344
+ converger.run do |event, data|
345
+ case event
346
+ when :success
347
+ reporter.success(TS.converge.success)
348
+ when :converge_error
349
+ reporter.error(TS.converge.failure)
350
+ when :creating_remote_policy
351
+ reporter.update(TS.converge.creating_remote_policy)
352
+ when :uploading_trusted_certs
353
+ reporter.update(TS.converge.uploading_trusted_certs)
354
+ when :running_chef
355
+ reporter.update(TS.converge.running_chef)
356
+ when :reboot
357
+ reporter.success(TS.converge.reboot)
358
+ else
359
+ handle_message(event, data, reporter)
360
+ end
361
+ end
362
+ end
363
+
364
+ def handle_perform_error(e)
365
+ id = e.respond_to?(:id) ? e.id : e.class.to_s
366
+ # TODO: This is currently sending host information for certain ssh errors
367
+ # post release we need to scrub this data. For now I'm redacting the
368
+ # whole message.
369
+ # message = e.respond_to?(:message) ? e.message : e.to_s
370
+ Telemeter.capture(:error, exception: { id: id, message: "redacted" })
371
+ wrapper = ChefApply::StandardErrorResolver.wrap_exception(e)
372
+ capture_exception_backtrace(wrapper)
373
+ # Now that our housekeeping is done, allow user-facing handling/formatting
374
+ # in `run` to execute by re-raising
375
+ raise wrapper
376
+ end
377
+
378
+ # When running multiple jobs, exceptions are captured to the
379
+ # job to avoid interrupting other jobs in process. This function
380
+ # collects them and raises a MultiJobFailure if failure has occurred;
381
+ # we do *not* differentiate between one failed jobs and multiple failed jobs
382
+ # - if you're in the 'multi-job' path (eg, multiple targets) we handle
383
+ # all errors the same to provide a consistent UX when running with mulitiple targets.
384
+ def handle_job_failures(jobs)
385
+ failed_jobs = jobs.select { |j| !j.exception.nil? }
386
+ return if failed_jobs.empty?
387
+ raise ChefApply::MultiJobFailure.new(failed_jobs)
388
+ end
389
+
390
+ # A handler for common action messages
391
+ def handle_message(message, data, reporter)
392
+ if message == :error # data[0] = exception
393
+ # Mark the current task as failed with whatever data is available to us
394
+ reporter.error(ChefApply::UI::ErrorPrinter.error_summary(data[0]))
395
+ end
396
+ end
397
+
398
+ def capture_exception_backtrace(e)
399
+ UI::ErrorPrinter.write_backtrace(e, @argv)
400
+ end
401
+
402
+ def show_help
403
+ UI::Terminal.output format_help
404
+ end
405
+
406
+ def do_connect(target_host, reporter, update_method)
407
+ target_host.connect!
408
+ reporter.send(update_method, T.status.connected)
409
+ rescue StandardError => e
410
+ message = ChefApply::UI::ErrorPrinter.error_summary(e)
411
+ reporter.error(message)
412
+ raise
413
+ end
414
+
415
+ def format_help
416
+ help_text = banner.clone # This prevents us appending to the banner text
417
+ help_text << "\n"
418
+ help_text << format_flags
419
+ end
420
+
421
+ def format_flags
422
+ flag_text = "FLAGS:\n"
423
+ justify_length = 0
424
+ options.each_value do |spec|
425
+ justify_length = [justify_length, spec[:long].length + 4].max
426
+ end
427
+ options.sort.to_h.each_value do |flag_spec|
428
+ short = flag_spec[:short] || " "
429
+ short = short[0, 2] # We only want the flag portion, not the capture portion (if present)
430
+ if short == " "
431
+ short = " "
432
+ else
433
+ short = "#{short}, "
434
+ end
435
+ flags = "#{short}#{flag_spec[:long]}"
436
+ flag_text << " #{flags.ljust(justify_length)} "
437
+ ml_padding = " " * (justify_length + 8)
438
+ first = true
439
+ flag_spec[:description].split("\n").each do |d|
440
+ flag_text << ml_padding unless first
441
+ first = false
442
+ flag_text << "#{d}\n"
443
+ end
444
+ end
445
+ flag_text
446
+ end
447
+
448
+ def usage
449
+ T.usage
450
+ end
451
+
452
+ def show_version
453
+ UI::Terminal.output T.version.show(ChefApply::VERSION)
454
+ end
455
+
456
+ class OptionValidationError < ChefApply::ErrorNoLogs
457
+ attr_reader :command
458
+ def initialize(id, calling_command, *args)
459
+ super(id, *args)
460
+ # TODO - this is getting cumbersome - move them to constructor options hash in base
461
+ @decorate = false
462
+ @command = calling_command
463
+ end
464
+ end
465
+
466
+ class PolicyfileInstallError < ChefApply::Error
467
+ def initialize(cause_err); super("CHEFPOLICY001", cause_err.message); end
468
+ end
469
+ end
470
+ end