kitchen-omnibus-chef 0.0.1

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.
@@ -0,0 +1,760 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2013, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # https://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "fileutils" unless defined?(FileUtils)
19
+ require "pathname" unless defined?(Pathname)
20
+ require "json" unless defined?(JSON)
21
+ require "cgi" unless defined?(CGI)
22
+ require "kitchen/util"
23
+
24
+ require_relative "chef/policyfile"
25
+ require_relative "chef/berkshelf"
26
+ require_relative "chef/common_sandbox"
27
+
28
+ module LicenseAcceptance
29
+ autoload :Acceptor, "license_acceptance/acceptor"
30
+ end
31
+
32
+ begin
33
+ require "chef-config/config"
34
+ require "chef-config/workstation_config_loader"
35
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
36
+ # This space left intentionally blank.
37
+ end
38
+
39
+ module Kitchen
40
+ module Provisioner
41
+ # Common implementation details for Chef-related provisioners.
42
+ #
43
+ # @author Fletcher Nichol <fnichol@nichol.ca>
44
+ class ChefBase < Base
45
+ default_config :require_chef_omnibus, true
46
+ default_config :chef_omnibus_url, "https://omnitruck.chef.io/install.sh"
47
+ default_config :chef_omnibus_install_options, nil
48
+ default_config :chef_license, nil
49
+ default_config :run_list, []
50
+ default_config :policy_group, nil
51
+ default_config :attributes, {}
52
+ default_config :config_path, nil
53
+ default_config :log_file, nil
54
+ default_config :log_level do |provisioner|
55
+ provisioner[:debug] ? "debug" : "auto"
56
+ end
57
+ default_config :profile_ruby, false
58
+ # The older policyfile_zero used `policyfile` so support it for compat.
59
+ default_config :policyfile, nil
60
+ # Will try to autodetect by searching for `Policyfile.rb` if not set.
61
+ # If set, will error if the file doesn't exist.
62
+ default_config :policyfile_path, nil
63
+ # Will try to autodetect by searching for `Berksfile` if not set.
64
+ # If set, will error if the file doesn't exist.
65
+ default_config :berksfile_path, nil
66
+ # If set to true (which is the default from `chef generate`), try to update
67
+ # backend cookbook downloader on every kitchen run.
68
+ default_config :always_update_cookbooks, true
69
+ default_config :cookbook_files_glob, %w(
70
+ README.* VERSION metadata.{json,rb} attributes.rb recipe.rb
71
+ attributes/**/* definitions/**/* files/**/* libraries/**/*
72
+ providers/**/* recipes/**/* resources/**/* templates/**/*
73
+ ohai/**/* compliance/**/*
74
+ ).join(",")
75
+ # to ease upgrades, allow the user to turn deprecation warnings into errors
76
+ default_config :deprecations_as_errors, false
77
+
78
+ # Override the default from Base so reboot handling works by default for Chef.
79
+ default_config :retry_on_exit_code, [35, 213]
80
+
81
+ default_config :multiple_converge, 1
82
+
83
+ default_config :enforce_idempotency, false
84
+
85
+ default_config :data_path do |provisioner|
86
+ provisioner.calculate_path("data")
87
+ end
88
+ expand_path_for :data_path
89
+
90
+ default_config :data_bags_path do |provisioner|
91
+ provisioner.calculate_path("data_bags")
92
+ end
93
+ expand_path_for :data_bags_path
94
+
95
+ default_config :environments_path do |provisioner|
96
+ provisioner.calculate_path("environments")
97
+ end
98
+ expand_path_for :environments_path
99
+
100
+ default_config :nodes_path do |provisioner|
101
+ provisioner.calculate_path("nodes")
102
+ end
103
+ expand_path_for :nodes_path
104
+
105
+ default_config :roles_path do |provisioner|
106
+ provisioner.calculate_path("roles")
107
+ end
108
+ expand_path_for :roles_path
109
+
110
+ default_config :clients_path do |provisioner|
111
+ provisioner.calculate_path("clients")
112
+ end
113
+ expand_path_for :clients_path
114
+
115
+ default_config :encrypted_data_bag_secret_key_path do |provisioner|
116
+ provisioner.calculate_path("encrypted_data_bag_secret_key", type: :file)
117
+ end
118
+ expand_path_for :encrypted_data_bag_secret_key_path
119
+
120
+ #
121
+ # New configuration options per RFC 091
122
+ # https://github.com/chef/chef-rfc/blob/master/rfc091-deprecate-kitchen-settings.md
123
+ #
124
+
125
+ # Setting product_name to nil. It is currently the pivot point
126
+ # between the two install paths (Mixlib::Install::ScriptGenerator and Mixlib::Install)
127
+ default_config :product_name
128
+
129
+ default_config :product_version, :latest
130
+
131
+ default_config :channel, :stable
132
+
133
+ default_config :install_strategy, "once"
134
+
135
+ default_config :platform
136
+
137
+ default_config :platform_version
138
+
139
+ default_config :architecture
140
+
141
+ default_config :download_url
142
+
143
+ default_config :checksum
144
+
145
+ deprecate_config_for :require_chef_omnibus do |provisioner|
146
+ case
147
+ when provisioner[:require_chef_omnibus] == false
148
+ Util.outdent!(<<-MSG)
149
+ The 'require_chef_omnibus' attribute with value of 'false' will
150
+ change to use the new 'install_strategy' attribute with a value of 'skip'.
151
+
152
+ Note: 'product_name' must be set in order to use 'install_strategy'.
153
+ Although this seems counterintuitive, it is necessary until
154
+ 'product_name' replaces 'require_chef_omnibus' as the default.
155
+
156
+ # New Usage #
157
+ provisioner:
158
+ product_name: <chef or chef-workstation>
159
+ install_strategy: skip
160
+ MSG
161
+ when provisioner[:require_chef_omnibus].to_s.match?(/\d/)
162
+ Util.outdent!(<<-MSG)
163
+ The 'require_chef_omnibus' attribute with version values will change
164
+ to use the new 'product_version' attribute.
165
+
166
+ Note: 'product_name' must be set in order to use 'product_version'
167
+ until 'product_name' replaces 'require_chef_omnibus' as the default.
168
+
169
+ # New Usage #
170
+ provisioner:
171
+ product_name: <chef or chef-workstation>
172
+ product_version: #{provisioner[:require_chef_omnibus]}
173
+ MSG
174
+ when provisioner[:require_chef_omnibus] == "latest"
175
+ Util.outdent!(<<-MSG)
176
+ The 'require_chef_omnibus' attribute with value of 'latest' will change
177
+ to use the new 'install_strategy' attribute with a value of 'always'.
178
+
179
+ Note: 'product_name' must be set in order to use 'install_strategy'
180
+ until 'product_name' replaces 'require_chef_omnibus' as the default.
181
+
182
+ # New Usage #
183
+ provisioner:
184
+ product_name: <chef or chef-workstation>
185
+ install_strategy: always
186
+ MSG
187
+ end
188
+ end
189
+
190
+ deprecate_config_for :chef_omnibus_url, Util.outdent!(<<-MSG)
191
+ Changing the 'chef_omnibus_url' attribute breaks existing functionality. It will
192
+ be removed in a future version.
193
+ MSG
194
+
195
+ deprecate_config_for :chef_omnibus_install_options, Util.outdent!(<<-MSG)
196
+ The 'chef_omnibus_install_options' attribute will be replaced by using
197
+ 'product_name' and 'channel' attributes.
198
+
199
+ Note: 'product_name' must be set in order to use 'channel'
200
+ until 'product_name' replaces 'require_chef_omnibus' as the default.
201
+
202
+ # Deprecated Example #
203
+ provisioner:
204
+ chef_omnibus_install_options: -P chef-workstation -c current
205
+
206
+ # New Usage #
207
+ provisioner:
208
+ product_name: chef-workstation
209
+ channel: current
210
+ MSG
211
+
212
+ deprecate_config_for :install_msi_url, Util.outdent!(<<-MSG)
213
+ The 'install_msi_url' will be relaced by the 'download_url' attribute.
214
+ 'download_url' will be applied to Bourne and PowerShell download scripts.
215
+
216
+ Note: 'product_name' must be set in order to use 'download_url'
217
+ until 'product_name' replaces 'require_chef_omnibus' as the default.
218
+
219
+ # New Usage #
220
+ provisioner:
221
+ product_name: <chef or chef-workstation>
222
+ download_url: http://direct-download-url
223
+ MSG
224
+
225
+ deprecate_config_for :chef_metadata_url, Util.outdent!(<<-MSG)
226
+ The 'chef_metadata_url' will be removed. The Windows metadata URL will be
227
+ fully managed by using attribute settings.
228
+ MSG
229
+
230
+ # Reads the local Chef::Config object (if present). We do this because
231
+ # we want to start bring Chef config and Chef Workstation config closer
232
+ # together. For example, we want to configure proxy settings in 1
233
+ # location instead of 3 configuration files.
234
+ #
235
+ # @param config [Hash] initial provided configuration
236
+ def initialize(config = {})
237
+ super(config)
238
+
239
+ if defined?(ChefConfig::WorkstationConfigLoader)
240
+ ChefConfig::WorkstationConfigLoader.new(config[:config_path]).load
241
+ end
242
+ # This exports any proxy config present in the Chef config to
243
+ # appropriate environment variables, which Test Kitchen respects
244
+ ChefConfig::Config.export_proxies if defined?(ChefConfig::Config.export_proxies)
245
+ end
246
+
247
+ def doctor(state)
248
+ deprecated_config = instance.driver.instance_variable_get(:@deprecated_config)
249
+ deprecated_config.each do |attr, msg|
250
+ info("**** #{attr} deprecated\n#{msg}")
251
+ end
252
+ end
253
+
254
+ # gives us the product version from either require_chef_omnibus or product_version
255
+ # If the non-default (true) value of require_chef_omnibus is present use that
256
+ # otherwise use config[:product_version] which defaults to :latest and is the actual
257
+ # default for chef provisioners
258
+ #
259
+ # @return [String,Symbol,NilClass] version or nil if not applicable
260
+ def product_version
261
+ case config[:require_chef_omnibus]
262
+ when FalseClass
263
+ nil
264
+ when TrueClass
265
+ config[:product_version]
266
+ else
267
+ config[:require_chef_omnibus]
268
+ end
269
+ end
270
+
271
+ # If the user has policyfiles we shell out to the `chef` executable, so need to ensure they have
272
+ # accepted the Chef Workstation license. Otherwise they just need the Chef Infra license.
273
+ #
274
+ # @return [String] license id to prompt for acceptance
275
+ def license_acceptance_id
276
+ case
277
+ when File.exist?(policyfile) && (config[:product_name].nil? || config[:product_name].start_with?("chef"))
278
+ "chef-workstation"
279
+ when config[:product_name]
280
+ config[:product_name]
281
+ else
282
+ "chef"
283
+ end
284
+ end
285
+
286
+ # (see Base#check_license)
287
+ def check_license
288
+ unless config[:download_url]
289
+ warn(
290
+ <<~WARNING
291
+ =====================================================================================================
292
+ \e[1m\e[93m!!!WARNING!!! Omnitruck downloads are being shutdown for specific Chef Infra Client versions
293
+ and will stop working entirely in the future. It is recommended to switch to using the new
294
+ kitchen-chef-enterprise plugin found with chef-test-kitchen-enterprise and bundled in chef-workstation 26.x+.
295
+
296
+ Please refer to this blog for schedule of which chef-client versions and when they will be affected:
297
+ https://www.chef.io/blog/decoding-the-change-progress-chef-is-moving-to-licensed-downloads
298
+
299
+ For non chef customers or community users it is recommended to switch to the kitchen-cinc plugin and cinc
300
+ provisioners like cinc_infra.\e[0m
301
+ =====================================================================================================
302
+ WARNING
303
+ )
304
+ end
305
+
306
+ name = license_acceptance_id
307
+ version = product_version
308
+ debug("Checking if we need to prompt for license acceptance on product: #{name} version: #{version}.")
309
+
310
+ acceptor = LicenseAcceptance::Acceptor.new(logger: Kitchen.logger, provided: config[:chef_license])
311
+ if acceptor.license_required?(name, version)
312
+ debug("License acceptance required for #{name} version: #{version}. Prompting")
313
+ license_id = acceptor.id_from_mixlib(name)
314
+ begin
315
+ acceptor.check_and_persist(license_id, version.to_s)
316
+ rescue LicenseAcceptance::LicenseNotAcceptedError => e
317
+ error("Cannot converge without accepting the #{e.product.pretty_name} License. Set it in your kitchen.yml or using the CHEF_LICENSE environment variable")
318
+ raise
319
+ end
320
+ config[:chef_license] ||= acceptor.acceptance_value
321
+ end
322
+ end
323
+
324
+ # (see Base#create_sandbox)
325
+ def create_sandbox
326
+ super
327
+ sanity_check_sandbox_options!
328
+ Chef::CommonSandbox.new(config, sandbox_path, instance).populate
329
+ end
330
+
331
+ # (see Base#init_command)
332
+ def init_command
333
+ dirs = %w{
334
+ cookbooks data data_bags environments roles clients
335
+ encrypted_data_bag_secret
336
+ }.sort.map { |dir| remote_path_join(config[:root_path], dir) }
337
+
338
+ vars = if powershell_shell?
339
+ init_command_vars_for_powershell(dirs)
340
+ else
341
+ init_command_vars_for_bourne(dirs)
342
+ end
343
+
344
+ prefix_command(shell_code_from_file(vars, "chef_base_init_command"))
345
+ end
346
+
347
+ # (see Base#install_command)
348
+ def install_command
349
+ return unless config[:require_chef_omnibus] || config[:product_name]
350
+ return if config[:product_name] && config[:install_strategy] == "skip"
351
+
352
+ prefix_command(install_script_contents)
353
+ end
354
+
355
+ private
356
+
357
+ def last_exit_code
358
+ "; exit $LastExitCode" if powershell_shell?
359
+ end
360
+
361
+ # @return [Hash] an option hash for the install commands
362
+ # @api private
363
+ def install_options
364
+ add_omnibus_directory_option if instance.driver.cache_directory
365
+ project = /\s*-P (\w+)\s*/.match(config[:chef_omnibus_install_options])
366
+ {
367
+ omnibus_url: config[:chef_omnibus_url],
368
+ project: project.nil? ? nil : project[1],
369
+ install_flags: config[:chef_omnibus_install_options],
370
+ sudo_command:,
371
+ }.tap do |opts|
372
+ opts[:root] = config[:chef_omnibus_root] if config.key? :chef_omnibus_root
373
+ %i{install_msi_url http_proxy https_proxy}.each do |key|
374
+ opts[key] = config[key] if config.key? key
375
+ end
376
+ end
377
+ end
378
+
379
+ # Verify if the "omnibus_dir_option" has already been passed, if so we
380
+ # don't use the @driver.cache_directory
381
+ #
382
+ # @api private
383
+ def add_omnibus_directory_option
384
+ cache_dir_option = "#{omnibus_dir_option} #{instance.driver.cache_directory}"
385
+ if config[:chef_omnibus_install_options].nil?
386
+ config[:chef_omnibus_install_options] = cache_dir_option
387
+ elsif config[:chef_omnibus_install_options].match(/\s*#{omnibus_dir_option}\s*/).nil?
388
+ config[:chef_omnibus_install_options] << " " << cache_dir_option
389
+ end
390
+ end
391
+
392
+ # @return [String] an absolute path to a Policyfile, relative to the
393
+ # kitchen root
394
+ # @api private
395
+ def policyfile
396
+ policyfile_basename = config[:policyfile_path] || config[:policyfile] || "Policyfile.rb"
397
+ File.expand_path(policyfile_basename, config[:kitchen_root])
398
+ end
399
+
400
+ # @return [String] an absolute path to a Berksfile, relative to the
401
+ # kitchen root
402
+ # @api private
403
+ def berksfile
404
+ berksfile_basename = config[:berksfile_path] || config[:berksfile] || "Berksfile"
405
+ File.expand_path(berksfile_basename, config[:kitchen_root])
406
+ end
407
+
408
+ # Generates a Hash with default values for a solo.rb or client.rb Chef
409
+ # configuration file.
410
+ #
411
+ # @return [Hash] a configuration hash
412
+ # @api private
413
+ def default_config_rb # rubocop:disable Metrics/MethodLength
414
+ root = config[:root_path].gsub("$env:TEMP", "\#{ENV['TEMP']}")
415
+
416
+ config_rb = {
417
+ node_name: instance.name,
418
+ checksum_path: remote_path_join(root, "checksums"),
419
+ file_cache_path: remote_path_join(root, "cache"),
420
+ file_backup_path: remote_path_join(root, "backup"),
421
+ cookbook_path: [
422
+ remote_path_join(root, "cookbooks"),
423
+ remote_path_join(root, "site-cookbooks"),
424
+ ],
425
+ data_bag_path: remote_path_join(root, "data_bags"),
426
+ environment_path: remote_path_join(root, "environments"),
427
+ node_path: remote_path_join(root, "nodes"),
428
+ role_path: remote_path_join(root, "roles"),
429
+ client_path: remote_path_join(root, "clients"),
430
+ user_path: remote_path_join(root, "users"),
431
+ validation_key: remote_path_join(root, "validation.pem"),
432
+ client_key: remote_path_join(root, "client.pem"),
433
+ chef_server_url: "http://127.0.0.1:8889",
434
+ encrypted_data_bag_secret: remote_path_join(
435
+ root, "encrypted_data_bag_secret"
436
+ ),
437
+ treat_deprecation_warnings_as_errors: config[:deprecations_as_errors],
438
+ }
439
+ config_rb[:chef_license] = config[:chef_license] unless config[:chef_license].nil?
440
+ config_rb
441
+ end
442
+
443
+ # Generates a rendered client.rb/solo.rb/knife.rb formatted file as a
444
+ # String.
445
+ #
446
+ # @param data [Hash] a key/value pair hash of configuration
447
+ # @return [String] a rendered Chef config file as a String
448
+ # @api private
449
+ def format_config_file(data)
450
+ data.each.map do |attr, value|
451
+ [attr, format_value(value)].join(" ")
452
+ end.join("\n")
453
+ end
454
+
455
+ # Converts a Ruby object to a String interpretation suitable for writing
456
+ # out to a client.rb/solo.rb/knife.rb file.
457
+ #
458
+ # @param obj [Object] an object
459
+ # @return [String] a string representation
460
+ # @api private
461
+ def format_value(obj)
462
+ if obj.is_a?(String) && obj =~ /^:/
463
+ obj
464
+ elsif obj.is_a?(String)
465
+ %{"#{obj.gsub("\\", "\\\\\\\\")}"}
466
+ elsif obj.is_a?(Array)
467
+ %{[#{obj.map { |i| format_value(i) }.join(", ")}]}
468
+ else
469
+ obj.inspect
470
+ end
471
+ end
472
+
473
+ # Generates the init command variables for Bourne shell-based platforms.
474
+ #
475
+ # @param dirs [Array<String>] directories
476
+ # @return [String] shell variable lines
477
+ # @api private
478
+ def init_command_vars_for_bourne(dirs)
479
+ [
480
+ shell_var("sudo_rm", sudo("rm")),
481
+ shell_var("dirs", dirs.join(" ")),
482
+ shell_var("root_path", config[:root_path]),
483
+ ].join("\n")
484
+ end
485
+
486
+ # Generates the init command variables for PowerShell-based platforms.
487
+ #
488
+ # @param dirs [Array<String>] directories
489
+ # @return [String] shell variable lines
490
+ # @api private
491
+ def init_command_vars_for_powershell(dirs)
492
+ [
493
+ %{$dirs = @(#{dirs.map { |d| %{"#{d}"} }.join(", ")})},
494
+ shell_var("root_path", config[:root_path]),
495
+ ].join("\n")
496
+ end
497
+
498
+ # Load cookbook dependency resolver code, if required.
499
+ #
500
+ # (see Base#load_needed_dependencies!)
501
+ def load_needed_dependencies!
502
+ super
503
+ if File.exist?(policyfile)
504
+ debug("Policyfile found at #{policyfile}, using Policyfile to resolve cookbook dependencies")
505
+ Chef::Policyfile.load!(logger:)
506
+ elsif File.exist?(berksfile)
507
+ debug("Berksfile found at #{berksfile}, using Berkshelf to resolve cookbook dependencies")
508
+ Chef::Berkshelf.load!(logger:)
509
+ end
510
+ end
511
+
512
+ # @return [String] contents of the install script
513
+ # @api private
514
+ def install_script_contents
515
+ # by default require_chef_omnibus is set to true. Check config[:product_name] first
516
+ # so that we can use it if configured.
517
+ if config[:product_name]
518
+ script_for_product
519
+ elsif config[:require_chef_omnibus]
520
+ script_for_omnibus_version
521
+ end
522
+ end
523
+
524
+ # @return [String] contents of product based install script
525
+ # @api private
526
+ def script_for_product
527
+ require "mixlib/install"
528
+ installer = Mixlib::Install.new({
529
+ product_name: config[:product_name],
530
+ product_version: config[:product_version],
531
+ channel: config[:channel].to_sym,
532
+ install_command_options: {
533
+ install_strategy: config[:install_strategy],
534
+ },
535
+ }.tap do |opts|
536
+ opts[:shell_type] = :ps1 if powershell_shell?
537
+ %i{platform platform_version architecture}.each do |key|
538
+ opts[key] = config[key] if config[key]
539
+ end
540
+
541
+ unless windows_os?
542
+ # omnitruck installer does not currently support a tmp dir option on windows
543
+ opts[:install_command_options][:tmp_dir] = config[:root_path]
544
+ opts[:install_command_options]["TMPDIR"] = config[:root_path]
545
+ end
546
+
547
+ if config[:download_url]
548
+ opts[:install_command_options][:download_url_override] = config[:download_url]
549
+ opts[:install_command_options][:checksum] = config[:checksum] if config[:checksum]
550
+ end
551
+
552
+ if instance.driver.cache_directory
553
+ download_dir_option = windows_os? ? :download_directory : :cmdline_dl_dir
554
+ opts[:install_command_options][download_dir_option] = instance.driver.cache_directory
555
+ end
556
+
557
+ proxies = {}.tap do |prox|
558
+ %i{http_proxy https_proxy ftp_proxy no_proxy}.each do |key|
559
+ prox[key] = config[key] if config[key]
560
+ end
561
+
562
+ # install.ps1 only supports http_proxy
563
+ prox.delete_if { |p| %i{https_proxy ftp_proxy no_proxy}.include?(p) } if powershell_shell?
564
+ end
565
+ opts[:install_command_options].merge!(proxies)
566
+ end)
567
+ config[:chef_omnibus_root] = installer.root
568
+ if powershell_shell?
569
+ installer.install_command
570
+ else
571
+ install_from_file(installer.install_command)
572
+ end
573
+ end
574
+
575
+ # @return [String] Correct option per platform to specify the the
576
+ # cache directory
577
+ # @api private
578
+ def omnibus_dir_option
579
+ windows_os? ? "-download_directory" : "-d"
580
+ end
581
+
582
+ def install_from_file(command)
583
+ install_file = "#{config[:root_path]}/chef-installer.sh"
584
+ script = []
585
+ script << "mkdir -p #{config[:root_path]}"
586
+ script << "if [ $? -ne 0 ]; then"
587
+ script << " echo Kitchen config setting root_path: '#{config[:root_path]}' not creatable by regular user "
588
+ script << " exit 1"
589
+ script << "fi"
590
+ script << "cat > #{install_file} <<\"EOL\""
591
+ script << command
592
+ script << "EOL"
593
+ script << "chmod +x #{install_file}"
594
+ script << sudo(install_file)
595
+ script.join("\n")
596
+ end
597
+
598
+ # @return [String] contents of version based install script
599
+ # @api private
600
+ def script_for_omnibus_version
601
+ require "mixlib/install/script_generator"
602
+ installer = Mixlib::Install::ScriptGenerator.new(
603
+ config[:require_chef_omnibus], powershell_shell?, install_options
604
+ )
605
+ config[:chef_omnibus_root] = installer.root
606
+ sudo(installer.install_command)
607
+ end
608
+
609
+ # Hook used in subclasses to indicate support for policyfiles.
610
+ #
611
+ # @abstract
612
+ # @return [Boolean]
613
+ # @api private
614
+ def supports_policyfile?
615
+ false
616
+ end
617
+
618
+ # @return [void]
619
+ # @raise [UserError]
620
+ # @api private
621
+ def sanity_check_sandbox_options!
622
+ if (config[:policyfile_path] || config[:policyfile]) && !File.exist?(policyfile)
623
+ raise UserError, "policyfile_path set in config " \
624
+ "(#{config[:policyfile_path]} could not be found. " \
625
+ "Expected to find it at full path #{policyfile}."
626
+ end
627
+ if config[:berksfile_path] && !File.exist?(berksfile)
628
+ raise UserError, "berksfile_path set in config " \
629
+ "(#{config[:berksfile_path]} could not be found. " \
630
+ "Expected to find it at full path #{berksfile}."
631
+ end
632
+ if File.exist?(policyfile) && !supports_policyfile?
633
+ raise UserError, "policyfile detected, but provisioner " \
634
+ "#{self.class.name} doesn't support Policyfiles. " \
635
+ "Either use a different provisioner, or delete/rename " \
636
+ "#{policyfile}."
637
+ end
638
+ end
639
+
640
+ # Writes a configuration file to the sandbox directory.
641
+ # @api private
642
+ def prepare_config_rb
643
+ data = default_config_rb.merge(config[config_filename.tr(".", "_").to_sym])
644
+ data = data.merge(named_run_list: config[:named_run_list]) if config[:named_run_list]
645
+
646
+ info("Preparing #{config_filename}")
647
+ debug("Creating #{config_filename} from #{data.inspect}")
648
+
649
+ File.open(File.join(sandbox_path, config_filename), "wb") do |file|
650
+ file.write(format_config_file(data))
651
+ end
652
+
653
+ prepare_config_idempotency_check(data) if config[:enforce_idempotency]
654
+ end
655
+
656
+ # Writes a configuration file to the sandbox directory
657
+ # to check for idempotency of the run.
658
+ # @api private
659
+ def prepare_config_idempotency_check(data)
660
+ handler_filename = "chef-client-fail-if-update-handler.rb"
661
+ source = File.join(
662
+ File.dirname(__FILE__), %w{.. .. .. support }, handler_filename
663
+ )
664
+ FileUtils.cp(source, File.join(sandbox_path, handler_filename))
665
+ File.open(File.join(sandbox_path, "client_no_updated_resources.rb"), "wb") do |file|
666
+ file.write(format_config_file(data))
667
+ file.write("\n\n")
668
+ file.write("handler_file = File.join(File.dirname(__FILE__), '#{handler_filename}')\n")
669
+ file.write "Chef::Config.from_file(handler_file)\n"
670
+ end
671
+ end
672
+
673
+ # Returns an Array of command line arguments for the chef client.
674
+ #
675
+ # @return [Array<String>] an array of command line arguments
676
+ # @api private
677
+ def chef_args(_config_filename)
678
+ raise "You must override in sub classes!"
679
+ end
680
+
681
+ # Returns a filename for the configuration file
682
+ # defaults to client.rb
683
+ #
684
+ # @return [String] a filename
685
+ # @api private
686
+ def config_filename
687
+ "client.rb"
688
+ end
689
+
690
+ # Gives the command used to run chef
691
+ # @api private
692
+ def chef_cmd(base_cmd)
693
+ if windows_os?
694
+ separator = [
695
+ "; if ($LastExitCode -ne 0) { ",
696
+ "throw \"Command failed with exit code $LastExitCode.\" } ;",
697
+ ].join
698
+ else
699
+ separator = " && "
700
+ end
701
+ chef_cmds(base_cmd).join(separator)
702
+ end
703
+
704
+ # Gives an array of commands
705
+ # @api private
706
+ def chef_cmds(base_cmd)
707
+ cmds = []
708
+ num_converges = config[:multiple_converge].to_i
709
+ idempotency = config[:enforce_idempotency]
710
+
711
+ # Execute Chef Client n-1 times, without exiting
712
+ (num_converges - 1).times do
713
+ cmds << wrapped_chef_cmd(base_cmd, config_filename)
714
+ end
715
+
716
+ # Append another execution with Windows specific Exit code helper or (for
717
+ # idempotency check) a specific config file which assures no changed resources.
718
+ cmds << unless idempotency
719
+ wrapped_chef_cmd(base_cmd, config_filename, append: last_exit_code)
720
+ else
721
+ wrapped_chef_cmd(base_cmd, "client_no_updated_resources.rb", append: last_exit_code)
722
+ end
723
+ cmds
724
+ end
725
+
726
+ # Concatenate all arguments and wrap it with shell-specifics
727
+ # @api private
728
+ def wrapped_chef_cmd(base_cmd, configfile, append: "")
729
+ args = []
730
+
731
+ args << base_cmd
732
+ args << chef_args(configfile)
733
+ args << append
734
+
735
+ shell_cmd = args.flatten.join(" ")
736
+ shell_cmd = shell_cmd.prepend(reload_ps1_path) if windows_os?
737
+
738
+ prefix_command(wrap_shell_code(shell_cmd))
739
+ end
740
+
741
+ # Builds a complete command given a variables String preamble and a file
742
+ # containing shell code.
743
+ #
744
+ # @param vars [String] shell variables, as a String
745
+ # @param file [String] file basename (without extension) containing
746
+ # shell code
747
+ # @return [String] command
748
+ # @api private
749
+ def shell_code_from_file(vars, file)
750
+ src_file = File.join(
751
+ File.dirname(__FILE__),
752
+ %w{.. .. .. support},
753
+ file + (powershell_shell? ? ".ps1" : ".sh")
754
+ )
755
+
756
+ wrap_shell_code([vars, "", File.read(src_file)].join("\n"))
757
+ end
758
+ end
759
+ end
760
+ end