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.
- checksums.yaml +7 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +423 -0
- data/LICENSE +201 -0
- data/README.md +41 -0
- data/Rakefile +32 -0
- data/bin/chef-run +23 -0
- data/chef-apply.gemspec +67 -0
- data/i18n/en.yml +513 -0
- data/lib/chef_apply.rb +20 -0
- data/lib/chef_apply/action/base.rb +158 -0
- data/lib/chef_apply/action/converge_target.rb +173 -0
- data/lib/chef_apply/action/install_chef.rb +30 -0
- data/lib/chef_apply/action/install_chef/base.rb +137 -0
- data/lib/chef_apply/action/install_chef/linux.rb +38 -0
- data/lib/chef_apply/action/install_chef/windows.rb +54 -0
- data/lib/chef_apply/action/reporter.rb +39 -0
- data/lib/chef_apply/cli.rb +470 -0
- data/lib/chef_apply/cli_options.rb +145 -0
- data/lib/chef_apply/config.rb +150 -0
- data/lib/chef_apply/error.rb +108 -0
- data/lib/chef_apply/errors/ccr_failure_mapper.rb +93 -0
- data/lib/chef_apply/file_fetcher.rb +70 -0
- data/lib/chef_apply/log.rb +42 -0
- data/lib/chef_apply/recipe_lookup.rb +117 -0
- data/lib/chef_apply/startup.rb +162 -0
- data/lib/chef_apply/status_reporter.rb +42 -0
- data/lib/chef_apply/target_host.rb +233 -0
- data/lib/chef_apply/target_resolver.rb +202 -0
- data/lib/chef_apply/telemeter.rb +162 -0
- data/lib/chef_apply/telemeter/patch.rb +32 -0
- data/lib/chef_apply/telemeter/sender.rb +121 -0
- data/lib/chef_apply/temp_cookbook.rb +159 -0
- data/lib/chef_apply/text.rb +77 -0
- data/lib/chef_apply/ui/error_printer.rb +261 -0
- data/lib/chef_apply/ui/plain_text_element.rb +75 -0
- data/lib/chef_apply/ui/terminal.rb +94 -0
- data/lib/chef_apply/version.rb +20 -0
- 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
|