Class: ChefApply::Action::ConvergeTarget

Inherits:
Base
  • Object
show all
Defined in:
lib/chef_apply/action/converge_target.rb,
lib/chef_apply/action/converge_target/ccr_failure_mapper.rb

Defined Under Namespace

Classes: CCRFailureMapper, ConfigUploadFailed, HandlerUploadFailed, PolicyUploadFailed

Instance Attribute Summary

Attributes inherited from Base

#config, #target_host

Instance Method Summary collapse

Methods inherited from Base

#initialize, #name, #notify, #run

Constructor Details

This class inherits a constructor from ChefApply::Action::Base

Instance Method Details

#chef_report_pathObject



157
158
159
# File 'lib/chef_apply/action/converge_target.rb', line 157

def chef_report_path
  @chef_report_path ||= target_host.normalize_path(File.join(target_host.ws_cache_path, "cache", "run-report.json"))
end

#create_remote_config(dir) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/chef_apply/action/converge_target.rb', line 71

def create_remote_config(dir)
  remote_config_path = File.join(dir, "workstation.rb")

  workstation_rb = <<~EOM
    local_mode true
    color false
    cache_path "#{target_host.ws_cache_path}"
    chef_repo_path "#{target_host.ws_cache_path}"
    require_relative "reporter"
    reporter = ChefApply::Reporter.new
    report_handlers << reporter
    exception_handlers << reporter
  EOM

  unless ChefApply::Config.chef.chef_license.nil?
    workstation_rb << <<~EOM
      chef_license "#{ChefApply::Config.chef.chef_license}"
    EOM
  end

  # add the target host's log level value
  # (we don't set a location because we want output to
  #   go in stdout for reporting back to chef-apply)
  log_settings = ChefApply::Config.log
  unless log_settings.target_level.nil?
    workstation_rb << <<~EOM
      log_level :#{log_settings.target_level}
    EOM
  end

  # Maybe add data collector endpoint.
  dc = ChefApply::Config.data_collector
  if !dc.url.nil? && !dc.token.nil?
    workstation_rb << <<~EOM
      data_collector.server_url "#{dc.url}"
      data_collector.token "#{dc.token}"
      data_collector.mode :solo
      data_collector.organization "Chef Workstation"
    EOM
  end

  begin
    config_file = Tempfile.new
    config_file.write(workstation_rb)
    config_file.close
    target_host.upload_file(config_file.path, remote_config_path)
  rescue RuntimeError
    raise ConfigUploadFailed.new
  ensure
    config_file.unlink
  end
  remote_config_path
end

#create_remote_handler(remote_dir) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/chef_apply/action/converge_target.rb', line 125

def create_remote_handler(remote_dir)
  remote_handler_path = File.join(remote_dir, "reporter.rb")
  begin
    # TODO - why don't we upload the original remote_handler_path instead of making a temp copy?
    handler_file = Tempfile.new
    # TODO - ideally this is a resource in the gem, and not placed in with source files.
    handler_file.write(File.read(File.join(__dir__, "reporter.rb")))
    handler_file.close
    target_host.upload_file(handler_file.path, remote_handler_path)
  # TODO - should we be more specific in our error catch?
  rescue RuntimeError
    raise HandlerUploadFailed.new
  ensure
    handler_file.unlink
  end
  remote_handler_path
end

#create_remote_policy(local_policy_path, remote_dir_path) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/chef_apply/action/converge_target.rb', line 59

def create_remote_policy(local_policy_path, remote_dir_path)
  remote_policy_path = File.join(remote_dir_path, File.basename(local_policy_path))
  notify(:creating_remote_policy)
  begin
    target_host.upload_file(local_policy_path, remote_policy_path)
  rescue RuntimeError => e
    ChefApply::Log.error(e)
    raise PolicyUploadFailed.new
  end
  remote_policy_path
end

#handle_ccr_errorObject



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/chef_apply/action/converge_target.rb', line 161

def handle_ccr_error
  require_relative "converge_target/ccr_failure_mapper"
  mapper_opts = {}
  content = target_host.fetch_file_contents(chef_report_path)
  if content.nil?
    report = {}
    mapper_opts[:failed_report_path] = chef_report_path
    ChefApply::Log.error("Could not read remote report at #{chef_report_path}")
  else
    # We need to delete the stacktrace after copying it over. Otherwise if we get a
    # remote failure that does not write a chef stacktrace its possible to get an old
    # stale stacktrace.
    target_host.del_file(chef_report_path)
    report = JSON.parse(content)
    ChefApply::Log.error("Remote chef-client error follows:")
    ChefApply::Log.error(report["exception"])
  end
  mapper = ConvergeTarget::CCRFailureMapper.new(report["exception"], mapper_opts)
  mapper.raise_mapped_exception!
end

#perform_actionObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/chef_apply/action/converge_target.rb', line 27

def perform_action
  local_policy_path = config.delete :local_policy_path
  remote_tmp = target_host.temp_dir
  remote_dir_path = target_host.normalize_path(remote_tmp)
  # Ensure the directory is owned by the connecting user,
  # otherwise we won't be able to put things into it over scp as that user.
  remote_policy_path = create_remote_policy(local_policy_path, remote_dir_path)
  remote_config_path = create_remote_config(remote_dir_path)
  create_remote_handler(remote_dir_path)
  upload_trusted_certs(remote_dir_path)

  notify(:running_chef)
  # TODO - just teach target_host how to run_chef?
  cmd_str = run_chef_cmd(remote_dir_path,
    File.basename(remote_config_path),
    File.basename(remote_policy_path))
  c = target_host.run_command(cmd_str)
  target_host.del_dir(remote_dir_path)
  if c.exit_status == 0
    ChefApply::Log.info(c.stdout)
    notify(:success)
  elsif c.exit_status == 35
    notify(:reboot)
  else
    notify(:converge_error)
    ChefApply::Log.error("Error running command [#{cmd_str}]")
    ChefApply::Log.error("stdout: #{c.stdout}")
    ChefApply::Log.error("stderr: #{c.stderr}")
    handle_ccr_error
  end
end

#run_chef_cmd(working_dir, config_file, policy) ⇒ Object

Chef will try ‘downloading’ the policy from the internet unless we pass it a valid, local file in the working directory. By pointing it at a local file it will just copy it instead of trying to download it.

Chef 13 on Linux requires full path specifiers for –config and –recipe-url while on Chef 13 and 14 on Windows must use relative specifiers to prevent URI from causing an error (github.com/chef/chef/pull/7223/files).



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/chef_apply/action/converge_target.rb', line 189

def run_chef_cmd(working_dir, config_file, policy)
  case target_host.base_os
  when :windows
    "Set-Location -Path #{working_dir}; " +
      # We must 'wait' for chef-client to finish before changing directories and Out-Null does that
      "chef-client -z --config #{File.join(working_dir, config_file)} --recipe-url #{File.join(working_dir, policy)} | Out-Null; " +
      # We have to leave working dir so we don't hold a lock on it, which allows us to delete this tempdir later
      "Set-Location C:/; " +
      "exit $LASTEXITCODE"
  else
    # cd is shell a builtin, so we'll invoke bash. This also means all commands are executed
    # with sudo (as long as we are hard coding our sudo use)
    "bash -c 'cd #{working_dir}; /opt/chef/bin/chef-client -z --config #{File.join(working_dir, config_file)} --recipe-url #{File.join(working_dir, policy)}'"
  end
end

#upload_trusted_certs(dir) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/chef_apply/action/converge_target.rb', line 143

def upload_trusted_certs(dir)
  # TODO BOOTSTRAP - trusted certs dir and other config to be received as argument to constructor
  local_tcd = Chef::Util::PathHelper.escape_glob_dir(ChefApply::Config.chef.trusted_certs_dir)
  certs = Dir.glob(File.join(local_tcd, "*.{crt,pem}"))
  return if certs.empty?

  notify(:uploading_trusted_certs)
  remote_tcd = "#{dir}/trusted_certs"
  target_host.make_directory(remote_tcd)
  certs.each do |cert_file|
    target_host.upload_file(cert_file, "#{remote_tcd}/#{File.basename(cert_file)}")
  end
end