knife-windows 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +26 -26
  3. data/CHANGELOG.md +131 -121
  4. data/DOC_CHANGES.md +22 -14
  5. data/Gemfile +14 -13
  6. data/README.md +400 -392
  7. data/RELEASE_NOTES.md +2 -26
  8. data/appveyor.yml +39 -39
  9. data/ci.gemfile +16 -16
  10. data/knife-windows.gemspec +25 -25
  11. data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +246 -233
  12. data/lib/chef/knife/bootstrap_windows_base.rb +443 -454
  13. data/lib/chef/knife/bootstrap_windows_ssh.rb +116 -115
  14. data/lib/chef/knife/bootstrap_windows_winrm.rb +102 -95
  15. data/lib/chef/knife/core/windows_bootstrap_context.rb +378 -378
  16. data/lib/chef/knife/knife_windows_base.rb +33 -33
  17. data/lib/chef/knife/windows_cert_generate.rb +155 -155
  18. data/lib/chef/knife/windows_cert_install.rb +68 -68
  19. data/lib/chef/knife/windows_helper.rb +36 -36
  20. data/lib/chef/knife/windows_listener_create.rb +107 -107
  21. data/lib/chef/knife/winrm.rb +122 -122
  22. data/lib/chef/knife/winrm_base.rb +123 -117
  23. data/lib/chef/knife/winrm_knife_base.rb +306 -305
  24. data/lib/chef/knife/winrm_session.rb +97 -91
  25. data/lib/chef/knife/winrm_shared_options.rb +47 -47
  26. data/lib/chef/knife/wsman_endpoint.rb +44 -44
  27. data/lib/chef/knife/wsman_test.rb +118 -118
  28. data/lib/knife-windows/path_helper.rb +234 -234
  29. data/lib/knife-windows/version.rb +6 -6
  30. data/spec/assets/win_template_rendered_with_bootstrap_install_command.txt +223 -223
  31. data/spec/assets/win_template_rendered_with_bootstrap_install_command_on_12_5_client.txt +223 -223
  32. data/spec/assets/win_template_rendered_without_bootstrap_install_command.txt +335 -335
  33. data/spec/assets/win_template_rendered_without_bootstrap_install_command_on_12_5_client.txt +335 -335
  34. data/spec/dummy_winrm_connection.rb +21 -0
  35. data/spec/functional/bootstrap_download_spec.rb +236 -241
  36. data/spec/spec_helper.rb +94 -94
  37. data/spec/unit/knife/bootstrap_options_spec.rb +157 -155
  38. data/spec/unit/knife/bootstrap_template_spec.rb +98 -98
  39. data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +423 -426
  40. data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +177 -177
  41. data/spec/unit/knife/windows_cert_generate_spec.rb +90 -90
  42. data/spec/unit/knife/windows_cert_install_spec.rb +51 -51
  43. data/spec/unit/knife/windows_listener_create_spec.rb +76 -76
  44. data/spec/unit/knife/winrm_session_spec.rb +71 -76
  45. data/spec/unit/knife/winrm_spec.rb +500 -508
  46. data/spec/unit/knife/wsman_test_spec.rb +209 -209
  47. metadata +16 -17
  48. data/spec/dummy_winrm_service.rb +0 -24
@@ -1,454 +1,443 @@
1
- #
2
- # Author:: Seth Chisamore (<schisamo@chef.io>)
3
- # Copyright:: Copyright (c) 2011-2016 Chef Software, Inc.
4
- # License:: Apache License, Version 2.0
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
- # http://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
-
19
- require 'chef/knife'
20
- require 'chef/knife/bootstrap'
21
- require 'chef/encrypted_data_bag_item'
22
- require 'chef/knife/core/windows_bootstrap_context'
23
- require 'chef/knife/knife_windows_base'
24
- # Chef 11 PathHelper doesn't have #home
25
- #require 'chef/util/path_helper'
26
-
27
- class Chef
28
- class Knife
29
- module BootstrapWindowsBase
30
-
31
- include Chef::Knife::KnifeWindowsBase
32
-
33
- # :nodoc:
34
- # Would prefer to do this in a rational way, but can't be done b/c of
35
- # Mixlib::CLI's design :(
36
- def self.included(includer)
37
- includer.class_eval do
38
-
39
- deps do
40
- require 'readline'
41
- require 'chef/json_compat'
42
- end
43
-
44
- option :chef_node_name,
45
- :short => "-N NAME",
46
- :long => "--node-name NAME",
47
- :description => "The Chef node name for your new node"
48
-
49
- option :prerelease,
50
- :long => "--prerelease",
51
- :description => "Install the pre-release chef gems"
52
-
53
- option :bootstrap_version,
54
- :long => "--bootstrap-version VERSION",
55
- :description => "The version of Chef to install",
56
- :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
57
-
58
- option :bootstrap_proxy,
59
- :long => "--bootstrap-proxy PROXY_URL",
60
- :description => "The proxy server for the node being bootstrapped",
61
- :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
62
-
63
- option :bootstrap_no_proxy,
64
- :long => "--bootstrap-no-proxy [NO_PROXY_URL|NO_PROXY_IP]",
65
- :description => "Do not proxy locations for the node being bootstrapped; this option is used internally by Opscode",
66
- :proc => Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np }
67
-
68
- option :bootstrap_install_command,
69
- :long => "--bootstrap-install-command COMMANDS",
70
- :description => "Custom command to install chef-client",
71
- :proc => Proc.new { |ic| Chef::Config[:knife][:bootstrap_install_command] = ic }
72
-
73
- # DEPR: Remove this option in Chef 13
74
- option :distro,
75
- :short => "-d DISTRO",
76
- :long => "--distro DISTRO",
77
- :description => "Bootstrap a distro using a template. [DEPRECATED] Use -t / --bootstrap-template option instead.",
78
- :proc => Proc.new { |v|
79
- Chef::Log.warn("[DEPRECATED] -d / --distro option is deprecated. Use --bootstrap-template option instead.")
80
- v
81
- }
82
-
83
- option :bootstrap_template,
84
- :short => "-t TEMPLATE",
85
- :long => "--bootstrap-template TEMPLATE",
86
- :description => "Bootstrap Chef using a built-in or custom template. Set to the full path of an erb template or use one of the built-in templates."
87
-
88
- # DEPR: Remove this option in Chef 13
89
- option :template_file,
90
- :long => "--template-file TEMPLATE",
91
- :description => "Full path to location of template to use. [DEPRECATED] Use -t / --bootstrap-template option instead.",
92
- :proc => Proc.new { |v|
93
- Chef::Log.warn("[DEPRECATED] --template-file option is deprecated. Use --bootstrap-template option instead.")
94
- v
95
- }
96
-
97
- option :run_list,
98
- :short => "-r RUN_LIST",
99
- :long => "--run-list RUN_LIST",
100
- :description => "Comma separated list of roles/recipes to apply",
101
- :proc => lambda { |o| o.split(",") },
102
- :default => []
103
-
104
- option :hint,
105
- :long => "--hint HINT_NAME[=HINT_FILE]",
106
- :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
107
- :proc => Proc.new { |h|
108
- Chef::Config[:knife][:hints] ||= Hash.new
109
- name, path = h.split("=")
110
- Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new
111
- }
112
-
113
- option :first_boot_attributes,
114
- :short => "-j JSON_ATTRIBS",
115
- :long => "--json-attributes",
116
- :description => "A JSON string to be added to the first run of chef-client",
117
- :proc => lambda { |o| JSON.parse(o) },
118
- :default => nil
119
-
120
- option :first_boot_attributes_from_file,
121
- :long => "--json-attribute-file FILE",
122
- :description => "A JSON file to be used to the first run of chef-client",
123
- :proc => lambda { |o| Chef::JSONCompat.parse(File.read(o)) },
124
- :default => nil
125
-
126
- # Mismatch between option 'encrypted_data_bag_secret' and it's long value '--secret' is by design for compatibility
127
- option :encrypted_data_bag_secret,
128
- :short => "-s SECRET",
129
- :long => "--secret ",
130
- :description => "The secret key to use to decrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config.",
131
- :default => false
132
-
133
- # Mismatch between option 'encrypted_data_bag_secret_file' and it's long value '--secret-file' is by design for compatibility
134
- option :encrypted_data_bag_secret_file,
135
- :long => "--secret-file SECRET_FILE",
136
- :description => "A file containing the secret key to use to encrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config."
137
-
138
- option :auth_timeout,
139
- :long => "--auth-timeout MINUTES",
140
- :description => "The maximum time in minutes to wait to for authentication over the transport to the node to succeed. The default value is 2 minutes.",
141
- :default => 2
142
-
143
- option :node_ssl_verify_mode,
144
- :long => "--node-ssl-verify-mode [peer|none]",
145
- :description => "Whether or not to verify the SSL cert for all HTTPS requests.",
146
- :proc => Proc.new { |v|
147
- valid_values = ["none", "peer"]
148
- unless valid_values.include?(v)
149
- raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}"
150
- end
151
- }
152
-
153
- option :node_verify_api_cert,
154
- :long => "--[no-]node-verify-api-cert",
155
- :description => "Verify the SSL cert for HTTPS requests to the Chef server API.",
156
- :boolean => true
157
-
158
- option :msi_url,
159
- :short => "-u URL",
160
- :long => "--msi-url URL",
161
- :description => "Location of the Chef Client MSI. The default templates will prefer to download from this location. The MSI will be downloaded from chef.io if not provided.",
162
- :default => ''
163
-
164
- option :install_as_service,
165
- :long => "--install-as-service",
166
- :description => "Install chef-client as a Windows service",
167
- :default => false
168
-
169
- option :bootstrap_vault_file,
170
- :long => '--bootstrap-vault-file VAULT_FILE',
171
- :description => 'A JSON file with a list of vault(s) and item(s) to be updated'
172
-
173
- option :bootstrap_vault_json,
174
- :long => '--bootstrap-vault-json VAULT_JSON',
175
- :description => 'A JSON string with the vault(s) and item(s) to be updated'
176
-
177
- option :bootstrap_vault_item,
178
- :long => '--bootstrap-vault-item VAULT_ITEM',
179
- :description => 'A single vault and item to update as "vault:item"',
180
- :proc => Proc.new { |i|
181
- (vault, item) = i.split(/:/)
182
- Chef::Config[:knife][:bootstrap_vault_item] ||= {}
183
- Chef::Config[:knife][:bootstrap_vault_item][vault] ||= []
184
- Chef::Config[:knife][:bootstrap_vault_item][vault].push(item)
185
- Chef::Config[:knife][:bootstrap_vault_item]
186
- }
187
-
188
- option :policy_name,
189
- :long => "--policy-name POLICY_NAME",
190
- :description => "Policyfile name to use (--policy-group must also be given)",
191
- :default => nil
192
-
193
- option :policy_group,
194
- :long => "--policy-group POLICY_GROUP",
195
- :description => "Policy group name to use (--policy-name must also be given)",
196
- :default => nil
197
-
198
- option :tags,
199
- :long => "--tags TAGS",
200
- :description => "Comma separated list of tags to apply to the node",
201
- :proc => lambda { |o| o.split(/[\s,]+/) },
202
- :default => []
203
- end
204
- end
205
-
206
- def default_bootstrap_template
207
- "windows-chef-client-msi"
208
- end
209
-
210
- def bootstrap_template
211
- # The order here is important. We want to check if we have the new Chef 12 option is set first.
212
- # Knife cloud plugins unfortunately all set a default option for the :distro so it should be at
213
- # the end.
214
- config[:bootstrap_template] || config[:template_file] || config[:distro] || default_bootstrap_template
215
- end
216
-
217
- # TODO: This should go away when CHEF-2193 is fixed
218
- def load_template(template=nil)
219
- # Are we bootstrapping using an already shipped template?
220
-
221
- template = bootstrap_template
222
-
223
- # Use the template directly if it's a path to an actual file
224
- if File.exists?(template)
225
- Chef::Log.debug("Using the specified bootstrap template: #{File.dirname(template)}")
226
- return IO.read(template).chomp
227
- end
228
-
229
- # Otherwise search the template directories until we find the right one
230
- bootstrap_files = []
231
- bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap/templates', "#{template}.erb")
232
- bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir
233
- ::Knife::Windows::PathHelper.all_homes('.chef', 'bootstrap', "#{template}.erb") { |p| bootstrap_files << p }
234
- bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb"))
235
- bootstrap_files.flatten!
236
-
237
- template = Array(bootstrap_files).find do |bootstrap_template|
238
- Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}")
239
- ::File.exists?(bootstrap_template)
240
- end
241
-
242
- unless template
243
- ui.info("Can not find bootstrap definition for #{config[:distro]}")
244
- raise Errno::ENOENT
245
- end
246
-
247
- Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}")
248
-
249
- IO.read(template).chomp
250
- end
251
-
252
- def bootstrap_context
253
- @bootstrap_context ||= Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config)
254
- end
255
-
256
- def load_correct_secret
257
- knife_secret_file = Chef::Config[:knife][:encrypted_data_bag_secret_file]
258
- knife_secret = Chef::Config[:knife][:encrypted_data_bag_secret]
259
- cli_secret_file = config[:encrypted_data_bag_secret_file]
260
- cli_secret = config[:encrypted_data_bag_secret]
261
-
262
- cli_secret_file = nil if cli_secret_file == knife_secret_file
263
- cli_secret = nil if cli_secret == knife_secret
264
-
265
- cli_secret_file = Chef::EncryptedDataBagItem.load_secret(cli_secret_file) if cli_secret_file != nil
266
- knife_secret_file = Chef::EncryptedDataBagItem.load_secret(knife_secret_file) if knife_secret_file != nil
267
-
268
- cli_secret_file || cli_secret || knife_secret_file || knife_secret
269
- end
270
-
271
- def first_boot_attributes
272
- config[:first_boot_attributes] || config[:first_boot_attributes_from_file] || {}
273
- end
274
-
275
- def render_template(template=nil)
276
- config[:first_boot_attributes] = first_boot_attributes
277
- config[:secret] = load_correct_secret
278
- Erubis::Eruby.new(template).evaluate(bootstrap_context)
279
- end
280
-
281
- def bootstrap(proto=nil)
282
- if Chef::Config[:knife][:encrypted_data_bag_secret_file] || Chef::Config[:knife][:encrypted_data_bag_secret]
283
- warn_chef_config_secret_key
284
- end
285
-
286
- bootstrap_architecture = Chef::Config[:knife][:bootstrap_architecture]
287
- if bootstrap_architecture && ![:x86_64, :i386].include?(bootstrap_architecture.to_sym)
288
- raise "Valid values for the knife config :bootstrap_architecture are i386 or x86_64. Supplied value is #{bootstrap_architecture}"
289
- end
290
- if Chef::Config[:knife][:architecture]
291
- raise "Do not set :architecture in your knife config, use :bootstrap_architecture."
292
- end
293
-
294
- validate_name_args!
295
-
296
- # adding respond_to? so this works with pre 12.4 chef clients
297
- validate_options! if respond_to?(:validate_options!)
298
-
299
- @node_name = Array(@name_args).first
300
- # back compat--templates may use this setting:
301
- config[:server_name] = @node_name
302
-
303
- STDOUT.sync = STDERR.sync = true
304
-
305
- if Chef::VERSION.split('.').first.to_i == 11 && Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))
306
- ui.error("Unable to find validation key. Please verify your configuration file for validation_key config value.")
307
- exit 1
308
- end
309
-
310
- if (defined?(chef_vault_handler) && chef_vault_handler.doing_chef_vault?) ||
311
- (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
312
-
313
- unless locate_config_value(:chef_node_name)
314
- ui.error("You must pass a node name with -N when bootstrapping with user credentials")
315
- exit 1
316
- end
317
-
318
- client_builder.run
319
-
320
- if client_builder.respond_to?(:client)
321
- chef_vault_handler.run(client_builder.client)
322
- else
323
- chef_vault_handler.run(node_name: config[:chef_node_name])
324
- end
325
-
326
- bootstrap_context.client_pem = client_builder.client_path
327
-
328
- else
329
- ui.info("Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}...")
330
- ui.info("Delete your validation key in order to use your user credentials instead")
331
- ui.info("")
332
- end
333
-
334
- wait_for_remote_response( config[:auth_timeout].to_i )
335
-
336
- set_target_architecture(bootstrap_architecture)
337
-
338
- ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}")
339
- # create a bootstrap.bat file on the node
340
- # we have to run the remote commands in 2047 char chunks
341
- create_bootstrap_bat_command do |command_chunk|
342
- begin
343
- render_command_result = run_command(command_chunk)
344
- ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0
345
- render_command_result
346
- rescue SystemExit => e
347
- raise unless e.success?
348
- end
349
- end
350
-
351
- # execute the bootstrap.bat file
352
- bootstrap_command_result = run_command(bootstrap_command)
353
- ui.error("Bootstrap command returned #{bootstrap_command_result}") if bootstrap_command_result != 0
354
-
355
- bootstrap_command_result
356
- end
357
-
358
- protected
359
-
360
- # Default implementation -- override only if required by the transport
361
- def wait_for_remote_response(wait_max_minutes)
362
- end
363
-
364
- def bootstrap_command
365
- @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}"
366
- end
367
-
368
- def bootstrap_render_banner_command(chunk_num)
369
- "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}"
370
- end
371
-
372
- def escape_windows_batch_characters(line)
373
- # TODO: The commands are going to get redirected - do we need to escape &?
374
- line.gsub!(/[(<|>)^]/).each{|m| "^#{m}"}
375
- end
376
-
377
- def create_bootstrap_bat_command()
378
- chunk_num = 0
379
- bootstrap_bat = ""
380
- banner = bootstrap_render_banner_command(chunk_num += 1)
381
- render_template(load_template(config[:bootstrap_template])).each_line do |line|
382
- escape_windows_batch_characters(line)
383
- # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can
384
- # confidently prefix every actual command with &&.
385
- # TODO: Why does ^\n&& work directly through the commandline but not through SOAP?
386
- render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})"
387
- # Windows commands are limited to 8191 characters for machines running XP or higher but
388
- # this includes the length of environment variables after they have been expanded.
389
- # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner
390
- # and once in every command redirection), we simply guess and set the max to 5000.
391
- # TODO: When a more accurate method is available, fix this.
392
- if bootstrap_bat.length + render_line.length + banner.length > 5000
393
- # Can't fit it into this chunk? - flush (if necessary) and then try.
394
- # Do this first because banner.length might change (e.g. due to an extra digit) and
395
- # prevent a fit.
396
- unless bootstrap_bat.empty?
397
- yield banner + bootstrap_bat
398
- bootstrap_bat = ""
399
- banner = bootstrap_render_banner_command(chunk_num += 1)
400
- end
401
- # Will this ever fit?
402
- if render_line.length + banner.length > 5000
403
- raise "Command in bootstrap template too long by #{render_line.length + banner.length - 5000} characters : #{line}"
404
- end
405
- end
406
- bootstrap_bat << render_line
407
- end
408
- raise "Bootstrap template was empty! Check #{config[:bootstrap_template]}" if bootstrap_bat.empty?
409
- yield banner + bootstrap_bat
410
- end
411
-
412
- def bootstrap_bat_file
413
- @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\""
414
- end
415
-
416
- def warn_chef_config_secret_key
417
- ui.info "* " * 40
418
- ui.warn(<<-WARNING)
419
- \nSpecifying the encrypted data bag secret key using an 'encrypted_data_bag_secret'
420
- entry in 'knife.rb' is deprecated. Please use the '--secret' or '--secret-file'
421
- options of this command instead.
422
-
423
- #{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this
424
- behavior will be removed and any 'encrypted_data_bag_secret' entries in
425
- 'knife.rb' will be ignored completely.
426
- WARNING
427
- ui.info "* " * 40
428
- end
429
-
430
- # We allow the user to specify the desired architecture of Chef to install or we default
431
- # to whatever the target system is. We assume that we are only bootstrapping 1 node at a time
432
- # so we don't need to worry about multipe responses from this command.
433
- def set_target_architecture(bootstrap_architecture)
434
- session_results = relay_winrm_command("echo %PROCESSOR_ARCHITECTURE%")
435
- if session_results.empty? || session_results[0].stdout.strip.empty?
436
- raise "Response to 'echo %PROCESSOR_ARCHITECTURE%' command was invalid: #{session_results}"
437
- end
438
- current_architecture = session_results[0].stdout.strip == "X86" ? :i386 : :x86_64
439
-
440
- if bootstrap_architecture.nil?
441
- architecture = current_architecture
442
- elsif bootstrap_architecture == :x86_64 && current_architecture == :i386
443
- raise "You specified bootstrap_architecture as x86_64 but the target machine is i386. A 64 bit program cannot run on a 32 bit machine."
444
- else
445
- architecture = bootstrap_architecture
446
- end
447
-
448
- # The windows install script wants i686, not i386
449
- architecture = :i686 if architecture == :i386
450
- Chef::Config[:knife][:architecture] = architecture
451
- end
452
- end
453
- end
454
- end
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@chef.io>)
3
+ # Copyright:: Copyright (c) 2011-2016 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
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
+ # http://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
+
19
+ require 'chef/knife'
20
+ require 'chef/knife/bootstrap'
21
+ require 'chef/encrypted_data_bag_item'
22
+ require 'chef/knife/core/windows_bootstrap_context'
23
+ require 'chef/knife/knife_windows_base'
24
+ # Chef 11 PathHelper doesn't have #home
25
+ #require 'chef/util/path_helper'
26
+
27
+ class Chef
28
+ class Knife
29
+ module BootstrapWindowsBase
30
+
31
+ include Chef::Knife::KnifeWindowsBase
32
+
33
+ # :nodoc:
34
+ # Would prefer to do this in a rational way, but can't be done b/c of
35
+ # Mixlib::CLI's design :(
36
+ def self.included(includer)
37
+ includer.class_eval do
38
+
39
+ deps do
40
+ require 'readline'
41
+ require 'chef/json_compat'
42
+ end
43
+
44
+ option :chef_node_name,
45
+ :short => "-N NAME",
46
+ :long => "--node-name NAME",
47
+ :description => "The Chef node name for your new node"
48
+
49
+ option :prerelease,
50
+ :long => "--prerelease",
51
+ :description => "Install the pre-release chef gems"
52
+
53
+ option :bootstrap_version,
54
+ :long => "--bootstrap-version VERSION",
55
+ :description => "The version of Chef to install",
56
+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
57
+
58
+ option :bootstrap_proxy,
59
+ :long => "--bootstrap-proxy PROXY_URL",
60
+ :description => "The proxy server for the node being bootstrapped",
61
+ :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
62
+
63
+ option :bootstrap_no_proxy,
64
+ :long => "--bootstrap-no-proxy [NO_PROXY_URL|NO_PROXY_IP]",
65
+ :description => "Do not proxy locations for the node being bootstrapped; this option is used internally by Opscode",
66
+ :proc => Proc.new { |np| Chef::Config[:knife][:bootstrap_no_proxy] = np }
67
+
68
+ option :bootstrap_install_command,
69
+ :long => "--bootstrap-install-command COMMANDS",
70
+ :description => "Custom command to install chef-client",
71
+ :proc => Proc.new { |ic| Chef::Config[:knife][:bootstrap_install_command] = ic }
72
+
73
+ # DEPR: Remove this option in Chef 13
74
+ option :distro,
75
+ :short => "-d DISTRO",
76
+ :long => "--distro DISTRO",
77
+ :description => "Bootstrap a distro using a template. [DEPRECATED] Use -t / --bootstrap-template option instead.",
78
+ :proc => Proc.new { |v|
79
+ Chef::Log.warn("[DEPRECATED] -d / --distro option is deprecated. Use --bootstrap-template option instead.")
80
+ v
81
+ }
82
+
83
+ option :bootstrap_template,
84
+ :short => "-t TEMPLATE",
85
+ :long => "--bootstrap-template TEMPLATE",
86
+ :description => "Bootstrap Chef using a built-in or custom template. Set to the full path of an erb template or use one of the built-in templates."
87
+
88
+ # DEPR: Remove this option in Chef 13
89
+ option :template_file,
90
+ :long => "--template-file TEMPLATE",
91
+ :description => "Full path to location of template to use. [DEPRECATED] Use -t / --bootstrap-template option instead.",
92
+ :proc => Proc.new { |v|
93
+ Chef::Log.warn("[DEPRECATED] --template-file option is deprecated. Use --bootstrap-template option instead.")
94
+ v
95
+ }
96
+
97
+ option :run_list,
98
+ :short => "-r RUN_LIST",
99
+ :long => "--run-list RUN_LIST",
100
+ :description => "Comma separated list of roles/recipes to apply",
101
+ :proc => lambda { |o| o.split(",") },
102
+ :default => []
103
+
104
+ option :hint,
105
+ :long => "--hint HINT_NAME[=HINT_FILE]",
106
+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
107
+ :proc => Proc.new { |h|
108
+ Chef::Config[:knife][:hints] ||= Hash.new
109
+ name, path = h.split("=")
110
+ Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new
111
+ }
112
+
113
+ option :first_boot_attributes,
114
+ :short => "-j JSON_ATTRIBS",
115
+ :long => "--json-attributes",
116
+ :description => "A JSON string to be added to the first run of chef-client",
117
+ :proc => lambda { |o| JSON.parse(o) },
118
+ :default => nil
119
+
120
+ option :first_boot_attributes_from_file,
121
+ :long => "--json-attribute-file FILE",
122
+ :description => "A JSON file to be used to the first run of chef-client",
123
+ :proc => lambda { |o| Chef::JSONCompat.parse(File.read(o)) },
124
+ :default => nil
125
+
126
+ # Mismatch between option 'encrypted_data_bag_secret' and it's long value '--secret' is by design for compatibility
127
+ option :encrypted_data_bag_secret,
128
+ :short => "-s SECRET",
129
+ :long => "--secret ",
130
+ :description => "The secret key to use to decrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config.",
131
+ :default => false
132
+
133
+ # Mismatch between option 'encrypted_data_bag_secret_file' and it's long value '--secret-file' is by design for compatibility
134
+ option :encrypted_data_bag_secret_file,
135
+ :long => "--secret-file SECRET_FILE",
136
+ :description => "A file containing the secret key to use to encrypt data bag item values. Will be rendered on the node at c:/chef/encrypted_data_bag_secret and set in the rendered client config."
137
+
138
+ option :auth_timeout,
139
+ :long => "--auth-timeout MINUTES",
140
+ :description => "The maximum time in minutes to wait to for authentication over the transport to the node to succeed. The default value is 2 minutes.",
141
+ :default => 2
142
+
143
+ option :node_ssl_verify_mode,
144
+ :long => "--node-ssl-verify-mode [peer|none]",
145
+ :description => "Whether or not to verify the SSL cert for all HTTPS requests.",
146
+ :proc => Proc.new { |v|
147
+ valid_values = ["none", "peer"]
148
+ unless valid_values.include?(v)
149
+ raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}"
150
+ end
151
+ v
152
+ }
153
+
154
+ option :node_verify_api_cert,
155
+ :long => "--[no-]node-verify-api-cert",
156
+ :description => "Verify the SSL cert for HTTPS requests to the Chef server API.",
157
+ :boolean => true
158
+
159
+ option :msi_url,
160
+ :short => "-u URL",
161
+ :long => "--msi-url URL",
162
+ :description => "Location of the Chef Client MSI. The default templates will prefer to download from this location. The MSI will be downloaded from chef.io if not provided.",
163
+ :default => ''
164
+
165
+ option :install_as_service,
166
+ :long => "--install-as-service",
167
+ :description => "Install chef-client as a Windows service",
168
+ :default => false
169
+
170
+ option :bootstrap_vault_file,
171
+ :long => '--bootstrap-vault-file VAULT_FILE',
172
+ :description => 'A JSON file with a list of vault(s) and item(s) to be updated'
173
+
174
+ option :bootstrap_vault_json,
175
+ :long => '--bootstrap-vault-json VAULT_JSON',
176
+ :description => 'A JSON string with the vault(s) and item(s) to be updated'
177
+
178
+ option :bootstrap_vault_item,
179
+ :long => '--bootstrap-vault-item VAULT_ITEM',
180
+ :description => 'A single vault and item to update as "vault:item"',
181
+ :proc => Proc.new { |i|
182
+ (vault, item) = i.split(/:/)
183
+ Chef::Config[:knife][:bootstrap_vault_item] ||= {}
184
+ Chef::Config[:knife][:bootstrap_vault_item][vault] ||= []
185
+ Chef::Config[:knife][:bootstrap_vault_item][vault].push(item)
186
+ Chef::Config[:knife][:bootstrap_vault_item]
187
+ }
188
+
189
+ option :policy_name,
190
+ :long => "--policy-name POLICY_NAME",
191
+ :description => "Policyfile name to use (--policy-group must also be given)",
192
+ :default => nil
193
+
194
+ option :policy_group,
195
+ :long => "--policy-group POLICY_GROUP",
196
+ :description => "Policy group name to use (--policy-name must also be given)",
197
+ :default => nil
198
+
199
+ option :tags,
200
+ :long => "--tags TAGS",
201
+ :description => "Comma separated list of tags to apply to the node",
202
+ :proc => lambda { |o| o.split(/[\s,]+/) },
203
+ :default => []
204
+ end
205
+ end
206
+
207
+ def default_bootstrap_template
208
+ "windows-chef-client-msi"
209
+ end
210
+
211
+ def bootstrap_template
212
+ # The order here is important. We want to check if we have the new Chef 12 option is set first.
213
+ # Knife cloud plugins unfortunately all set a default option for the :distro so it should be at
214
+ # the end.
215
+ config[:bootstrap_template] || config[:template_file] || config[:distro] || default_bootstrap_template
216
+ end
217
+
218
+ # TODO: This should go away when CHEF-2193 is fixed
219
+ def load_template(template=nil)
220
+ # Are we bootstrapping using an already shipped template?
221
+
222
+ template = bootstrap_template
223
+
224
+ # Use the template directly if it's a path to an actual file
225
+ if File.exists?(template)
226
+ Chef::Log.debug("Using the specified bootstrap template: #{File.dirname(template)}")
227
+ return IO.read(template).chomp
228
+ end
229
+
230
+ # Otherwise search the template directories until we find the right one
231
+ bootstrap_files = []
232
+ bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap/templates', "#{template}.erb")
233
+ bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir
234
+ ::Knife::Windows::PathHelper.all_homes('.chef', 'bootstrap', "#{template}.erb") { |p| bootstrap_files << p }
235
+ bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb"))
236
+ bootstrap_files.flatten!
237
+
238
+ template = Array(bootstrap_files).find do |bootstrap_template|
239
+ Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}")
240
+ ::File.exists?(bootstrap_template)
241
+ end
242
+
243
+ unless template
244
+ ui.info("Can not find bootstrap definition for #{config[:distro]}")
245
+ raise Errno::ENOENT
246
+ end
247
+
248
+ Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}")
249
+
250
+ IO.read(template).chomp
251
+ end
252
+
253
+ def bootstrap_context
254
+ @bootstrap_context ||= Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config)
255
+ end
256
+
257
+ def load_correct_secret
258
+ knife_secret_file = Chef::Config[:knife][:encrypted_data_bag_secret_file]
259
+ knife_secret = Chef::Config[:knife][:encrypted_data_bag_secret]
260
+ cli_secret_file = config[:encrypted_data_bag_secret_file]
261
+ cli_secret = config[:encrypted_data_bag_secret]
262
+
263
+ cli_secret_file = nil if cli_secret_file == knife_secret_file
264
+ cli_secret = nil if cli_secret == knife_secret
265
+
266
+ cli_secret_file = Chef::EncryptedDataBagItem.load_secret(cli_secret_file) if cli_secret_file != nil
267
+ knife_secret_file = Chef::EncryptedDataBagItem.load_secret(knife_secret_file) if knife_secret_file != nil
268
+
269
+ cli_secret_file || cli_secret || knife_secret_file || knife_secret
270
+ end
271
+
272
+ def first_boot_attributes
273
+ config[:first_boot_attributes] || config[:first_boot_attributes_from_file] || {}
274
+ end
275
+
276
+ def render_template(template=nil)
277
+ config[:first_boot_attributes] = first_boot_attributes
278
+ config[:secret] = load_correct_secret
279
+ Erubis::Eruby.new(template).evaluate(bootstrap_context)
280
+ end
281
+
282
+ def bootstrap(proto=nil)
283
+ if Chef::Config[:knife][:encrypted_data_bag_secret_file] || Chef::Config[:knife][:encrypted_data_bag_secret]
284
+ warn_chef_config_secret_key
285
+ end
286
+
287
+ set_target_architecture
288
+
289
+ # adding respond_to? so this works with pre 12.4 chef clients
290
+ validate_options! if respond_to?(:validate_options!)
291
+
292
+ @node_name = Array(@name_args).first
293
+ # back compat--templates may use this setting:
294
+ config[:server_name] = @node_name
295
+
296
+ STDOUT.sync = STDERR.sync = true
297
+
298
+ if Chef::VERSION.split('.').first.to_i == 11 && Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))
299
+ ui.error("Unable to find validation key. Please verify your configuration file for validation_key config value.")
300
+ exit 1
301
+ end
302
+
303
+ if (defined?(chef_vault_handler) && chef_vault_handler.doing_chef_vault?) ||
304
+ (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
305
+
306
+ unless locate_config_value(:chef_node_name)
307
+ ui.error("You must pass a node name with -N when bootstrapping with user credentials")
308
+ exit 1
309
+ end
310
+
311
+ client_builder.run
312
+
313
+ if client_builder.respond_to?(:client)
314
+ chef_vault_handler.run(client_builder.client)
315
+ else
316
+ chef_vault_handler.run(node_name: config[:chef_node_name])
317
+ end
318
+
319
+ bootstrap_context.client_pem = client_builder.client_path
320
+
321
+ else
322
+ ui.info("Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}...")
323
+ ui.info("Delete your validation key in order to use your user credentials instead")
324
+ ui.info("")
325
+ end
326
+
327
+ wait_for_remote_response( config[:auth_timeout].to_i )
328
+
329
+ ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}")
330
+ # create a bootstrap.bat file on the node
331
+ # we have to run the remote commands in 2047 char chunks
332
+ create_bootstrap_bat_command do |command_chunk|
333
+ begin
334
+ render_command_result = run_command(command_chunk)
335
+ ui.error("Batch render command returned #{render_command_result}") if render_command_result != 0
336
+ render_command_result
337
+ rescue SystemExit => e
338
+ raise unless e.success?
339
+ end
340
+ end
341
+
342
+ # execute the bootstrap.bat file
343
+ bootstrap_command_result = run_command(bootstrap_command)
344
+ ui.error("Bootstrap command returned #{bootstrap_command_result}") if bootstrap_command_result != 0
345
+
346
+ bootstrap_command_result
347
+ end
348
+
349
+ protected
350
+
351
+ # Default implementation -- override only if required by the transport
352
+ def wait_for_remote_response(wait_max_minutes)
353
+ end
354
+
355
+ def bootstrap_command
356
+ @bootstrap_command ||= "cmd.exe /C #{bootstrap_bat_file}"
357
+ end
358
+
359
+ def bootstrap_render_banner_command(chunk_num)
360
+ "cmd.exe /C echo Rendering #{bootstrap_bat_file} chunk #{chunk_num}"
361
+ end
362
+
363
+ def escape_windows_batch_characters(line)
364
+ # TODO: The commands are going to get redirected - do we need to escape &?
365
+ line.gsub!(/[(<|>)^]/).each{|m| "^#{m}"}
366
+ end
367
+
368
+ def create_bootstrap_bat_command()
369
+ chunk_num = 0
370
+ bootstrap_bat = ""
371
+ banner = bootstrap_render_banner_command(chunk_num += 1)
372
+ render_template(load_template(config[:bootstrap_template])).each_line do |line|
373
+ escape_windows_batch_characters(line)
374
+ # We are guaranteed to have a prefix "banner" command that echo's chunk number. We can
375
+ # confidently prefix every actual command with &&.
376
+ # TODO: Why does ^\n&& work directly through the commandline but not through SOAP?
377
+ render_line = " && >> #{bootstrap_bat_file} (echo.#{line.chomp.strip})"
378
+ # Windows commands are limited to 8191 characters for machines running XP or higher but
379
+ # this includes the length of environment variables after they have been expanded.
380
+ # Since we don't actually know how long %TEMP% (and it's used twice - once in the banner
381
+ # and once in every command redirection), we simply guess and set the max to 5000.
382
+ # TODO: When a more accurate method is available, fix this.
383
+ if bootstrap_bat.length + render_line.length + banner.length > 5000
384
+ # Can't fit it into this chunk? - flush (if necessary) and then try.
385
+ # Do this first because banner.length might change (e.g. due to an extra digit) and
386
+ # prevent a fit.
387
+ unless bootstrap_bat.empty?
388
+ yield banner + bootstrap_bat
389
+ bootstrap_bat = ""
390
+ banner = bootstrap_render_banner_command(chunk_num += 1)
391
+ end
392
+ # Will this ever fit?
393
+ if render_line.length + banner.length > 5000
394
+ raise "Command in bootstrap template too long by #{render_line.length + banner.length - 5000} characters : #{line}"
395
+ end
396
+ end
397
+ bootstrap_bat << render_line
398
+ end
399
+ raise "Bootstrap template was empty! Check #{config[:bootstrap_template]}" if bootstrap_bat.empty?
400
+ yield banner + bootstrap_bat
401
+ end
402
+
403
+ def bootstrap_bat_file
404
+ @bootstrap_bat_file ||= "\"%TEMP%\\bootstrap-#{Process.pid}-#{Time.now.to_i}.bat\""
405
+ end
406
+
407
+ def warn_chef_config_secret_key
408
+ ui.info "* " * 40
409
+ ui.warn(<<-WARNING)
410
+ \nSpecifying the encrypted data bag secret key using an 'encrypted_data_bag_secret'
411
+ entry in 'knife.rb' is deprecated. Please use the '--secret' or '--secret-file'
412
+ options of this command instead.
413
+
414
+ #{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this
415
+ behavior will be removed and any 'encrypted_data_bag_secret' entries in
416
+ 'knife.rb' will be ignored completely.
417
+ WARNING
418
+ ui.info "* " * 40
419
+ end
420
+
421
+ # We allow the user to specify the desired architecture of Chef to install or we default
422
+ # to whatever the target system is.
423
+ # This is because a user might want to install a 32bit chef client on a 64bit machine
424
+ def set_target_architecture
425
+ if Chef::Config[:knife][:architecture]
426
+ raise "Do not set :architecture in your knife config, use :bootstrap_architecture."
427
+ end
428
+
429
+ if Chef::Config[:knife][:bootstrap_architecture]
430
+ bootstrap_architecture = Chef::Config[:knife][:bootstrap_architecture]
431
+
432
+ if ![:x86_64, :i386].include?(bootstrap_architecture.to_sym)
433
+ raise "Valid values for the knife config :bootstrap_architecture are i386 or x86_64. Supplied value is #{bootstrap_architecture}"
434
+ end
435
+
436
+ # The windows install script wants i686, not i386
437
+ bootstrap_architecture = :i686 if bootstrap_architecture == :i386
438
+ Chef::Config[:knife][:architecture] = bootstrap_architecture
439
+ end
440
+ end
441
+ end
442
+ end
443
+ end