class ChefApply::CLI

Constants

RC_COMMAND_FAILED
RC_ERROR_HANDLING_FAILED
RC_OK
RC_UNHANDLED_ERROR

Attributes

archive_file_location[R]
target_hosts[R]
temp_cookbook[R]

Public Class Methods

new(argv) click to toggle source
Calls superclass method
# File lib/chef_apply/cli.rb, line 60
def initialize(argv)
  @argv = argv.clone
  @rc = RC_OK
  super()
end

Public Instance Methods

capture_exception_backtrace(e) click to toggle source
# File lib/chef_apply/cli.rb, line 327
def capture_exception_backtrace(e)
  UI::ErrorPrinter.write_backtrace(e, @argv)
end
check_license_acceptance() click to toggle source
# 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) click to toggle source

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

# 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) click to toggle source

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

# 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) click to toggle source
# 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) click to toggle source

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

# 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) click to toggle source

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

# 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) click to toggle source
# 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) click to toggle source

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)

# 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) click to toggle source

A handler for common action messages

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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