chef-apply 0.1.2 → 0.1.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,24 +20,15 @@ require "fileutils"
20
20
 
21
21
  module ChefApply::Action::InstallChef
22
22
  class Base < ChefApply::Action::Base
23
- MIN_CHEF_VERSION = Gem::Version.new("13.0.0")
23
+ MIN_14_VERSION = Gem::Version.new("14.1.1")
24
+ MIN_13_VERSION = Gem::Version.new("13.10.0")
24
25
 
25
26
  def perform_action
26
- if target_host.installed_chef_version >= MIN_CHEF_VERSION
27
+ if check_minimum_chef_version!(target_host) == :minimum_version_met
27
28
  notify(:already_installed)
28
- return
29
+ else
30
+ perform_local_install
29
31
  end
30
- raise ClientOutdated.new(target_host.installed_chef_version, MIN_CHEF_VERSION)
31
- # NOTE: 2018-05-10 below is an intentionally dead code path that
32
- # will get re-visited once we determine how we want automatic
33
- # upgrades to behave.
34
- # @upgrading = true
35
- # perform_local_install
36
- rescue ChefApply::TargetHost::ChefNotInstalled
37
- if config[:check_only]
38
- raise ClientNotInstalled.new()
39
- end
40
- perform_local_install
41
32
  end
42
33
 
43
34
  def name
@@ -116,6 +107,31 @@ module ChefApply::Action::InstallChef
116
107
  remote_path
117
108
  end
118
109
 
110
+ def check_minimum_chef_version!(target)
111
+ begin
112
+ installed_version = target.installed_chef_version
113
+ rescue ChefApply::TargetHost::ChefNotInstalled
114
+ if config[:check_only]
115
+ raise ClientNotInstalled.new()
116
+ end
117
+ return :client_not_installed
118
+ end
119
+
120
+ case
121
+ when installed_version >= Gem::Version.new("14.0.0") && installed_version < MIN_14_VERSION
122
+ raise Client14Outdated.new(installed_version, MIN_14_VERSION)
123
+ when installed_version >= Gem::Version.new("13.0.0") && installed_version < MIN_13_VERSION
124
+ raise Client13Outdated.new(installed_version, MIN_13_VERSION, MIN_14_VERSION)
125
+ when installed_version < Gem::Version.new("13.0.0")
126
+ # If they have Chef < 13.0.0 installed we want to show them the easiest upgrade path -
127
+ # Chef 13 first and then Chef 14 since most customers cannot make the leap directly
128
+ # to 14.
129
+ raise Client13Outdated.new(installed_version, MIN_13_VERSION, MIN_14_VERSION)
130
+ end
131
+
132
+ :minimum_version_met
133
+ end
134
+
119
135
  def setup_remote_temp_path
120
136
  raise NotImplementedError
121
137
  end
@@ -129,9 +145,15 @@ module ChefApply::Action::InstallChef
129
145
  def initialize(); super("CHEFINS002"); end
130
146
  end
131
147
 
132
- class ClientOutdated < ChefApply::ErrorNoLogs
148
+ class Client13Outdated < ChefApply::ErrorNoLogs
149
+ def initialize(current_version, min_13_version, min_14_version)
150
+ super("CHEFINS003", current_version, min_13_version, min_14_version)
151
+ end
152
+ end
153
+
154
+ class Client14Outdated < ChefApply::ErrorNoLogs
133
155
  def initialize(current_version, target_version)
134
- super("CHEFINS003", current_version, target_version)
156
+ super("CHEFINS004", current_version, target_version)
135
157
  end
136
158
  end
137
159
  end
@@ -1,4 +1,5 @@
1
1
  #
2
+ #p
2
3
  # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
4
  # License:: Apache License, Version 2.0
4
5
  #
@@ -15,30 +16,37 @@
15
16
  # limitations under the License.
16
17
  #
17
18
  require "mixlib/cli"
18
- require "chef/log"
19
+
20
+ require "chef_apply/config"
19
21
  require "chef-config/config"
20
22
  require "chef-config/logger"
21
23
 
22
- require "chef_apply/cli_options"
24
+ require "chef_apply/cli/validation"
25
+ require "chef_apply/cli/options"
26
+ require "chef_apply/cli/help"
23
27
  require "chef_apply/action/converge_target"
28
+ require "chef_apply/action/generate_local_policy"
29
+ require "chef_apply/action/generate_temp_cookbook"
24
30
  require "chef_apply/action/install_chef"
25
- require "chef_apply/config"
26
31
  require "chef_apply/error"
27
32
  require "chef_apply/log"
28
- require "chef_apply/recipe_lookup"
29
33
  require "chef_apply/target_host"
30
34
  require "chef_apply/target_resolver"
31
35
  require "chef_apply/telemeter"
32
- require "chef_apply/telemeter/sender"
33
- require "chef_apply/temp_cookbook"
34
- require "chef_apply/ui/terminal"
35
36
  require "chef_apply/ui/error_printer"
36
- require "chef_apply/version"
37
+ require "chef_apply/ui/terminal"
37
38
 
38
39
  module ChefApply
39
40
  class CLI
41
+ attr_reader :temp_cookbook, :archive_file_location, :target_hosts
42
+
40
43
  include Mixlib::CLI
41
- include ChefApply::CLIOptions
44
+ # Pulls in the CLI options and flags we have defined for this command.
45
+ include ChefApply::CLI::Options
46
+ # Argument validation and parsing behaviors
47
+ include ChefApply::CLI::Validation
48
+ # Help and version formatting
49
+ include ChefApply::CLI::Help
42
50
 
43
51
  RC_OK = 0
44
52
  RC_COMMAND_FAILED = 1
@@ -98,27 +106,11 @@ module ChefApply
98
106
  show_version
99
107
  else
100
108
  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
109
+ target_hosts = resolve_targets(cli_arguments.shift, parsed_options)
110
+ render_cookbook_setup(cli_arguments)
111
+ render_converge(target_hosts)
120
112
  end
121
- rescue OptionParser::InvalidOption => e
113
+ rescue OptionParser::InvalidOption => e # from parse_options
122
114
  # Using nil here is a bit gross but it prevents usage from printing.
123
115
  ove = OptionValidationError.new("CHEFVAL010", nil,
124
116
  e.message.split(":")[1].strip, # only want the flag
@@ -131,181 +123,47 @@ module ChefApply
131
123
  temp_cookbook.delete unless temp_cookbook.nil?
132
124
  end
133
125
 
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
126
+ def resolve_targets(host_spec, opts)
127
+ @target_hosts = TargetResolver.new(host_spec,
128
+ opts.delete(:protocol),
129
+ opts).targets
147
130
  end
148
131
 
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)
132
+ def render_cookbook_setup(arguments)
133
+ UI::Terminal.render_job(TS.generate_temp_cookbook.generating) do |reporter|
134
+ @temp_cookbook = generate_temp_cookbook(arguments, reporter)
154
135
  end
155
- UI::Terminal.render_job(initial_status_msg, prefix: "[#{target_host.hostname}]") do |reporter|
156
- converge(reporter, local_policy_path, target_host)
136
+ UI::Terminal.render_job(TS.generate_temp_cookbook.generating) do |reporter|
137
+ @archive_file_location = generate_local_policy(reporter)
157
138
  end
158
139
  end
159
140
 
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.
141
+ def render_converge(target_hosts)
163
142
  jobs = target_hosts.map do |target_host|
164
- # This block will run in its own thread during render.
143
+ # Each block will run in its own thread during render.
165
144
  UI::Terminal::Job.new("[#{target_host.hostname}]", target_host) do |reporter|
166
145
  connect_target(target_host, reporter)
167
- reporter.update(TS.install_chef.verifying)
168
146
  install(target_host, reporter)
169
- reporter.update(initial_status_msg)
170
- converge(reporter, local_policy_path, target_host)
147
+ converge(reporter, archive_file_location, target_host)
171
148
  end
172
149
  end
173
- UI::Terminal.render_parallel_jobs(TS.converge.multi_header, jobs)
150
+ header = TS.converge.header(target_hosts.length, temp_cookbook.descriptor, temp_cookbook.from)
151
+ UI::Terminal.render_parallel_jobs(header, jobs)
174
152
  handle_job_failures(jobs)
175
153
  end
176
154
 
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
155
+ # Accepts a target_host and establishes the connection to that host
156
+ # while providing visual feedback via the Terminal API.
157
+ def connect_target(target_host, reporter)
158
+ connect_message = T.status.connecting(target_host.user)
159
+ reporter.update(connect_message)
160
+ do_connect(target_host, reporter)
302
161
  end
303
162
 
304
- # Runs the InstallChef action and renders UI updates as
305
- # the action reports back
306
163
  def install(target_host, reporter)
307
- installer = Action::InstallChef.instance_for_target(target_host, check_only: !parsed_options[:install])
308
164
  context = TS.install_chef
165
+ reporter.update(context.verifying)
166
+ installer = Action::InstallChef.instance_for_target(target_host, check_only: !parsed_options[:install])
309
167
  installer.run do |event, data|
310
168
  case event
311
169
  when :installing
@@ -320,39 +178,82 @@ module ChefApply
320
178
  when :downloading
321
179
  reporter.update(context.downloading)
322
180
  when :already_installed
323
- meth = @multi_target ? :update : :success
324
- reporter.send(meth, context.already_present(target_host.installed_chef_version))
181
+ reporter.update(context.already_present(target_host.installed_chef_version))
325
182
  when :install_complete
326
- meth = @multi_target ? :update : :success
327
183
  if installer.upgrading?
328
184
  message = context.upgrade_success(target_host.installed_chef_version, installer.version_to_install)
329
185
  else
330
186
  message = context.install_success(installer.version_to_install)
331
187
  end
332
- reporter.send(meth, message)
188
+ reporter.update(message)
189
+ else
190
+ handle_message(event, data, reporter)
191
+ end
192
+ end
193
+ end
194
+
195
+ # Runs a GenerateCookbook action based on recipe/resource infoprovided
196
+ # and renders UI updates as the action reports back
197
+ def generate_temp_cookbook(arguments, reporter)
198
+ opts = if arguments.length == 1
199
+ { recipe_spec: arguments.shift,
200
+ cookbook_repo_paths: parsed_options[:cookbook_repo_paths] }
201
+ else
202
+ { resource_type: arguments.shift,
203
+ resource_name: arguments.shift,
204
+ resource_properties: properties_from_string(arguments) }
205
+ end
206
+ action = ChefApply::Action::GenerateTempCookbook.from_options(opts)
207
+ action.run do |event, data|
208
+ case event
209
+ when :generating
210
+ reporter.update(TS.generate_temp_cookbook.generating)
211
+ when :success
212
+ reporter.success(TS.generate_temp_cookbook.success)
213
+ else
214
+ handle_message(event, data, reporter)
215
+ end
216
+ end
217
+ action.generated_cookbook
218
+ end
219
+
220
+ # Runs the GenerateLocalPolicy action and renders UI updates
221
+ # as the action reports back
222
+ def generate_local_policy(reporter)
223
+ action = Action::GenerateLocalPolicy.new(cookbook: temp_cookbook)
224
+ action.run do |event, data|
225
+ case event
226
+ when :generating
227
+ reporter.update(TS.generate_local_policy.generating)
228
+ when :exporting
229
+ reporter.update(TS.generate_local_policy.exporting)
230
+ when :success
231
+ reporter.success(TS.generate_local_policy.success)
333
232
  else
334
233
  handle_message(event, data, reporter)
335
234
  end
336
235
  end
236
+ action.archive_file_location
337
237
  end
338
238
 
339
239
  # Runs the Converge action and renders UI updates as
340
240
  # the action reports back
341
241
  def converge(reporter, local_policy_path, target_host)
242
+ reporter.update(TS.converge.converging(temp_cookbook.descriptor))
342
243
  converge_args = { local_policy_path: local_policy_path, target_host: target_host }
343
244
  converger = Action::ConvergeTarget.new(converge_args)
344
245
  converger.run do |event, data|
345
246
  case event
346
247
  when :success
347
- reporter.success(TS.converge.success)
248
+ reporter.success(TS.converge.success(temp_cookbook.descriptor))
348
249
  when :converge_error
349
- reporter.error(TS.converge.failure)
250
+ reporter.error(TS.converge.failure(temp_cookbook.descriptor))
350
251
  when :creating_remote_policy
351
252
  reporter.update(TS.converge.creating_remote_policy)
352
253
  when :uploading_trusted_certs
353
254
  reporter.update(TS.converge.uploading_trusted_certs)
354
255
  when :running_chef
355
- reporter.update(TS.converge.running_chef)
256
+ reporter.update(TS.converge.converging(temp_cookbook.descriptor))
356
257
  when :reboot
357
258
  reporter.success(TS.converge.reboot)
358
259
  else
@@ -362,13 +263,14 @@ module ChefApply
362
263
  end
363
264
 
364
265
  def handle_perform_error(e)
266
+ require "chef_apply/errors/standard_error_resolver"
365
267
  id = e.respond_to?(:id) ? e.id : e.class.to_s
366
268
  # TODO: This is currently sending host information for certain ssh errors
367
269
  # post release we need to scrub this data. For now I'm redacting the
368
270
  # whole message.
369
271
  # message = e.respond_to?(:message) ? e.message : e.to_s
370
272
  Telemeter.capture(:error, exception: { id: id, message: "redacted" })
371
- wrapper = ChefApply::StandardErrorResolver.wrap_exception(e)
273
+ wrapper = ChefApply::Errors::StandardErrorResolver.wrap_exception(e)
372
274
  capture_exception_backtrace(wrapper)
373
275
  # Now that our housekeeping is done, allow user-facing handling/formatting
374
276
  # in `run` to execute by re-raising
@@ -377,13 +279,16 @@ module ChefApply
377
279
 
378
280
  # When running multiple jobs, exceptions are captured to the
379
281
  # 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.
282
+ # collects them and raises directly (in the case of just one job in the list)
283
+ # or raises a MultiJobFailure (when more than one job was being run)
384
284
  def handle_job_failures(jobs)
385
285
  failed_jobs = jobs.select { |j| !j.exception.nil? }
386
286
  return if failed_jobs.empty?
287
+ if jobs.length == 1
288
+ # Don't provide a bad UX by showing a 'one or more jobs has failed'
289
+ # message when there was only one job.
290
+ raise jobs.first.exception
291
+ end
387
292
  raise ChefApply::MultiJobFailure.new(failed_jobs)
388
293
  end
389
294
 
@@ -399,72 +304,14 @@ module ChefApply
399
304
  UI::ErrorPrinter.write_backtrace(e, @argv)
400
305
  end
401
306
 
402
- def show_help
403
- UI::Terminal.output format_help
404
- end
405
-
406
- def do_connect(target_host, reporter, update_method)
307
+ def do_connect(target_host, reporter)
407
308
  target_host.connect!
408
- reporter.send(update_method, T.status.connected)
309
+ reporter.update(T.status.connected)
409
310
  rescue StandardError => e
410
311
  message = ChefApply::UI::ErrorPrinter.error_summary(e)
411
312
  reporter.error(message)
412
313
  raise
413
314
  end
414
315
 
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
316
  end
470
317
  end