Class: ChefApply::CLI

Inherits:
Object
  • Object
show all
Includes:
Help, Options, Validation, LicenseAcceptance::CLIFlags::MixlibCLI, Mixlib::CLI
Defined in:
lib/chef_apply/cli.rb,
lib/chef_apply/cli/help.rb,
lib/chef_apply/cli/options.rb,
lib/chef_apply/cli/validation.rb

Defined Under Namespace

Modules: Help, Options, Validation Classes: OptionValidationError

Constant Summary collapse

RC_OK =
0
RC_COMMAND_FAILED =
1
RC_UNHANDLED_ERROR =
32
RC_ERROR_HANDLING_FAILED =
64

Constants included from Help

Help::T

Constants included from Validation

Validation::CB_MATCHER, Validation::PROPERTY_MATCHER

Constants included from Options

Options::T, Options::TS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Help

#format_flags, #format_help, #show_help, #show_version, #usage

Methods included from Validation

#properties_from_string, #transform_property_value, #validate_params

Methods included from Options

included, #parsed_options

Constructor Details

#initialize(argv) ⇒ CLI

Returns a new instance of CLI.



60
61
62
63
64
# File 'lib/chef_apply/cli.rb', line 60

def initialize(argv)
  @argv = argv.clone
  @rc = RC_OK
  super()
end

Instance Attribute Details

#archive_file_locationObject (readonly)

Returns the value of attribute archive_file_location.



44
45
46
# File 'lib/chef_apply/cli.rb', line 44

def archive_file_location
  @archive_file_location
end

#target_hostsObject (readonly)

Returns the value of attribute target_hosts.



44
45
46
# File 'lib/chef_apply/cli.rb', line 44

def target_hosts
  @target_hosts
end

#temp_cookbookObject (readonly)

Returns the value of attribute temp_cookbook.



44
45
46
# File 'lib/chef_apply/cli.rb', line 44

def temp_cookbook
  @temp_cookbook
end

Instance Method Details

#capture_exception_backtrace(e) ⇒ Object



327
328
329
# File 'lib/chef_apply/cli.rb', line 327

def capture_exception_backtrace(e)
  UI::ErrorPrinter.write_backtrace(e, @argv)
end

#check_license_acceptanceObject



128
129
130
131
132
133
134
135
136
# File 'lib/chef_apply/cli.rb', line 128

def check_license_acceptance
  acceptor = LicenseAcceptance::Acceptor.new(provided: ChefApply::Config.chef.chef_license)
  begin
    acceptor.check_and_persist("infra-client", "latest")
  rescue LicenseAcceptance::LicenseNotAcceptedError
    raise LicenseCheckFailed.new
  end
  ChefApply::Config.chef.chef_license ||= acceptor.acceptance_value
end

#connect_target(target_host, reporter) ⇒ Object

Accepts a target_host and establishes the connection to that host while providing visual feedback via the Terminal API.



175
176
177
178
179
# File 'lib/chef_apply/cli.rb', line 175

def connect_target(target_host, reporter)
  connect_message = T.status.connecting(target_host.user)
  reporter.update(connect_message)
  do_connect(target_host, reporter)
end

#converge(reporter, local_policy_path, target_host) ⇒ Object

Runs the Converge action and renders UI updates as the action reports back



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/chef_apply/cli.rb', line 260

def converge(reporter, local_policy_path, target_host)
  reporter.update(TS.converge.converging(temp_cookbook.descriptor))
  converge_args = { local_policy_path: local_policy_path, target_host: target_host }
  converger = Action::ConvergeTarget.new(converge_args)
  converger.run do |event, data|
    case event
    when :success
      reporter.success(TS.converge.success(temp_cookbook.descriptor))
    when :converge_error
      reporter.error(TS.converge.failure(temp_cookbook.descriptor))
    when :creating_remote_policy
      reporter.update(TS.converge.creating_remote_policy)
    when :uploading_trusted_certs
      reporter.update(TS.converge.uploading_trusted_certs)
    when :running_chef
      reporter.update(TS.converge.converging(temp_cookbook.descriptor))
    when :reboot
      reporter.success(TS.converge.reboot)
    else
      handle_message(event, data, reporter)
    end
  end
end

#do_connect(target_host, reporter) ⇒ Object



331
332
333
334
335
336
337
338
# File 'lib/chef_apply/cli.rb', line 331

def do_connect(target_host, reporter)
  target_host.connect!
  reporter.update(T.status.connected)
rescue StandardError => e
  message = ChefApply::UI::ErrorPrinter.error_summary(e)
  reporter.error(message)
  raise
end

#generate_local_policy(reporter) ⇒ Object

Runs the GenerateLocalPolicy action and renders UI updates as the action reports back



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/chef_apply/cli.rb', line 241

def generate_local_policy(reporter)
  action = Action::GenerateLocalPolicy.new(cookbook: temp_cookbook)
  action.run do |event, data|
    case event
    when :generating
      reporter.update(TS.generate_local_policy.generating)
    when :exporting
      reporter.update(TS.generate_local_policy.exporting)
    when :success
      reporter.success(TS.generate_local_policy.success)
    else
      handle_message(event, data, reporter)
    end
  end
  action.archive_file_location
end

#generate_temp_cookbook(arguments, reporter) ⇒ Object

Runs a GenerateCookbook action based on recipe/resource info provided and renders UI updates as the action reports back



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/chef_apply/cli.rb', line 216

def generate_temp_cookbook(arguments, reporter)
  opts = if arguments.length == 1
           { recipe_spec: arguments.shift,
             cookbook_repo_paths: parsed_options[:cookbook_repo_paths] }
         else
           { resource_type: arguments.shift,
             resource_name: arguments.shift,
             resource_properties: properties_from_string(arguments) }
         end
  action = ChefApply::Action::GenerateTempCookbook.from_options(opts)
  action.run do |event, data|
    case event
    when :generating
      reporter.update(TS.generate_temp_cookbook.generating)
    when :success
      reporter.success(TS.generate_temp_cookbook.success)
    else
      handle_message(event, data, reporter)
    end
  end
  action.generated_cookbook
end

#handle_failed_job(job) ⇒ Object



315
316
317
# File 'lib/chef_apply/cli.rb', line 315

def handle_failed_job(job)
  raise job.exception unless job.exception.nil?
end

#handle_failed_jobs(jobs) ⇒ Object

When running multiple jobs, exceptions are captured to the job to avoid interrupting other jobs in process. This function collects them and raises directly (in the case of just one job in the list) or raises a MultiJobFailure (when more than one job was being run)



303
304
305
306
307
308
309
310
311
312
313
# File 'lib/chef_apply/cli.rb', line 303

def handle_failed_jobs(jobs)
  failed_jobs = jobs.select { |j| !j.exception.nil? }
  return if failed_jobs.empty?
  if jobs.length == 1
    # Don't provide a bad UX by showing a 'one or more jobs has failed'
    # message when there was only one job.
    raise jobs.first.exception
  end

  raise ChefApply::MultiJobFailure.new(failed_jobs)
end

#handle_message(message, data, reporter) ⇒ Object

A handler for common action messages



320
321
322
323
324
325
# File 'lib/chef_apply/cli.rb', line 320

def handle_message(message, data, reporter)
  if message == :error # data[0] = exception
    # Mark the current task as failed with whatever data is available to us
    reporter.error(ChefApply::UI::ErrorPrinter.error_summary(data[0]))
  end
end

#handle_perform_error(e) ⇒ Object



284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/chef_apply/cli.rb', line 284

def handle_perform_error(e)
  require_relative "errors/standard_error_resolver"
  id = e.respond_to?(:id) ? e.id : e.class.to_s
  # TODO: This is currently sending host information for certain ssh errors
  #       post release we need to scrub this data. For now I'm redacting the
  #       whole message.
  # message = e.respond_to?(:message) ? e.message : e.to_s
  Telemeter.capture(:error, exception: { id: id, message: "redacted" })
  wrapper = ChefApply::Errors::StandardErrorResolver.wrap_exception(e)
  capture_exception_backtrace(wrapper)
  # Now that our housekeeping is done, allow user-facing handling/formatting
  # in `run` to execute by re-raising
  raise wrapper
end

#handle_run_error(e) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/chef_apply/cli.rb', line 85

def handle_run_error(e)
  case e
  when nil
    RC_OK
  when WrappedError
    UI::ErrorPrinter.show_error(e)
    RC_COMMAND_FAILED
  when SystemExit
    e.status
  when Exception
    UI::ErrorPrinter.dump_unexpected_error(e)
    RC_ERROR_HANDLING_FAILED
  else
    UI::ErrorPrinter.dump_unexpected_error(e)
    RC_UNHANDLED_ERROR
  end
end

#install(target_host, reporter) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/chef_apply/cli.rb', line 181

def install(target_host, reporter)
  require_relative "action/install_chef"
  context = TS.install_chef
  reporter.update(context.verifying)
  installer = Action::InstallChef.new(target_host: target_host, check_only: !parsed_options[:install])
  installer.run do |event, data|
    case event
    when :installing
      if installer.upgrading?
        message = context.upgrading(target_host.installed_chef_version, installer.version_to_install)
      else
        message = context.installing(installer.version_to_install)
      end
      reporter.update(message)
    when :uploading
      reporter.update(context.uploading)
    when :downloading
      reporter.update(context.downloading)
    when :already_installed
      reporter.update(context.already_present(target_host.installed_chef_version))
    when :install_complete
      if installer.upgrading?
        message = context.upgrade_success(target_host.installed_chef_version, installer.version_to_install)
      else
        message = context.install_success(installer.version_to_install)
      end
      reporter.update(message)
    else
      handle_message(event, data, reporter)
    end
  end
end

#perform_run(enforce_license: false) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/chef_apply/cli.rb', line 103

def perform_run(enforce_license: false)
  parse_options(@argv)
  if @argv.empty? || parsed_options[:help]
    show_help
  elsif parsed_options[:version]
    show_version
  else
    check_license_acceptance if enforce_license
    validate_params(cli_arguments)
    target_hosts = resolve_targets(cli_arguments.shift, parsed_options)
    render_cookbook_setup(cli_arguments)
    render_converge(target_hosts)
  end
rescue OptionParser::InvalidOption => e # from parse_options
  # Using nil here is a bit gross but it prevents usage from printing.
  ove = OptionValidationError.new("CHEFVAL010", nil,
    e.message.split(":")[1].strip, # only want the flag
    format_flags.lines[1..-1].join) # remove 'FLAGS:' header
  handle_perform_error(ove)
rescue => e
  handle_perform_error(e)
ensure
  temp_cookbook.delete unless temp_cookbook.nil?
end

#render_converge(target_hosts) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/chef_apply/cli.rb', line 159

def render_converge(target_hosts)
  jobs = target_hosts.map do |target_host|
    # Each block will run in its own thread during render.
    UI::Terminal::Job.new("[#{target_host.hostname}]", target_host) do |reporter|
      connect_target(target_host, reporter)
      install(target_host, reporter)
      converge(reporter, archive_file_location, target_host)
    end
  end
  header = TS.converge.header(target_hosts.length, temp_cookbook.descriptor, temp_cookbook.from)
  UI::Terminal.render_parallel_jobs(header, jobs)
  handle_failed_jobs(jobs)
end

#render_cookbook_setup(arguments) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/chef_apply/cli.rb', line 144

def render_cookbook_setup(arguments)
  # TODO update Job so that it doesn't require prefix and host. As a data container,
  # should these attributes even be required?
  job = UI::Terminal::Job.new("", nil) do |reporter|
    @temp_cookbook = generate_temp_cookbook(arguments, reporter)
  end
  UI::Terminal.render_job("...", job)
  handle_failed_job(job)
  job = UI::Terminal::Job.new("", nil) do |reporter|
    @archive_file_location = generate_local_policy(reporter)
  end
  UI::Terminal.render_job("...", job)
  handle_failed_job(job)
end

#resolve_targets(host_spec, opts) ⇒ Object



138
139
140
141
142
# File 'lib/chef_apply/cli.rb', line 138

def resolve_targets(host_spec, opts)
  @target_hosts = TargetResolver.new(host_spec,
    opts.delete(:protocol),
    opts).targets
end

#run(enforce_license: false) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/chef_apply/cli.rb', line 66

def run(enforce_license: false)
  # Perform a timing and capture of the run. Individual methods and actions may perform
  # nested Chef::Telemeter.timed_*_capture or Chef::Telemeter.capture calls in their operation, and
  # they will be captured in the same telemetry session.

  Chef::Telemeter.timed_run_capture([:redacted]) do

    perform_run(enforce_license: enforce_license)
  rescue Exception => e
    @rc = handle_run_error(e)

  end
rescue => e # can occur if exception thrown in error handling
  @rc = handle_run_error(e)
ensure
  Chef::Telemeter.commit
  exit @rc
end