knife-windows 0.8.6 → 1.0.0.rc.0

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +17 -3
  4. data/CHANGELOG.md +25 -6
  5. data/DOC_CHANGES.md +323 -0
  6. data/Gemfile +2 -1
  7. data/README.md +160 -29
  8. data/RELEASE_NOTES.md +59 -6
  9. data/appveyor.yml +42 -0
  10. data/ci.gemfile +15 -0
  11. data/knife-windows.gemspec +4 -2
  12. data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +35 -21
  13. data/lib/chef/knife/bootstrap_windows_base.rb +155 -31
  14. data/lib/chef/knife/bootstrap_windows_ssh.rb +1 -1
  15. data/lib/chef/knife/bootstrap_windows_winrm.rb +17 -10
  16. data/lib/chef/knife/core/windows_bootstrap_context.rb +67 -16
  17. data/lib/chef/knife/windows_cert_generate.rb +155 -0
  18. data/lib/chef/knife/windows_cert_install.rb +62 -0
  19. data/lib/chef/knife/windows_helper.rb +3 -1
  20. data/lib/chef/knife/windows_listener_create.rb +100 -0
  21. data/lib/chef/knife/winrm.rb +84 -208
  22. data/lib/chef/knife/winrm_base.rb +36 -10
  23. data/lib/chef/knife/winrm_knife_base.rb +201 -0
  24. data/lib/chef/knife/winrm_session.rb +72 -0
  25. data/lib/chef/knife/winrm_shared_options.rb +47 -0
  26. data/lib/chef/knife/wsman_endpoint.rb +44 -0
  27. data/lib/chef/knife/wsman_test.rb +96 -0
  28. data/lib/knife-windows/path_helper.rb +77 -0
  29. data/lib/knife-windows/version.rb +1 -1
  30. data/spec/functional/bootstrap_download_spec.rb +41 -23
  31. data/spec/spec_helper.rb +11 -1
  32. data/spec/unit/knife/bootstrap_template_spec.rb +27 -27
  33. data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +67 -23
  34. data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +47 -0
  35. data/spec/unit/knife/windows_cert_generate_spec.rb +90 -0
  36. data/spec/unit/knife/windows_cert_install_spec.rb +35 -0
  37. data/spec/unit/knife/windows_listener_create_spec.rb +61 -0
  38. data/spec/unit/knife/winrm_session_spec.rb +47 -0
  39. data/spec/unit/knife/winrm_spec.rb +222 -56
  40. data/spec/unit/knife/wsman_test_spec.rb +176 -0
  41. metadata +51 -20
@@ -53,7 +53,7 @@ class Chef
53
53
  :description => "The ssh port",
54
54
  :default => "22",
55
55
  :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
56
-
56
+
57
57
  option :ssh_gateway,
58
58
  :short => "-G GATEWAY",
59
59
  :long => "--ssh-gateway GATEWAY",
@@ -19,7 +19,8 @@
19
19
  require 'chef/knife/bootstrap_windows_base'
20
20
  require 'chef/knife/winrm'
21
21
  require 'chef/knife/winrm_base'
22
- require 'chef/knife/bootstrap'
22
+ require 'chef/knife/winrm_knife_base'
23
+
23
24
 
24
25
  class Chef
25
26
  class Knife
@@ -27,6 +28,7 @@ class Chef
27
28
 
28
29
  include Chef::Knife::BootstrapWindowsBase
29
30
  include Chef::Knife::WinrmBase
31
+ include Chef::Knife::WinrmCommandSharedFunctions
30
32
 
31
33
  deps do
32
34
  require 'chef/knife/core/windows_bootstrap_context'
@@ -38,27 +40,33 @@ class Chef
38
40
  banner "knife bootstrap windows winrm FQDN (options)"
39
41
 
40
42
  def run
43
+ if (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
44
+ if !negotiate_auth? && !(locate_config_value(:winrm_transport) == 'ssl')
45
+ ui.error("Validatorless bootstrap over unsecure winrm channels could expose your key to network sniffing")
46
+ exit 1
47
+ end
48
+ end
41
49
  bootstrap
42
50
  end
43
51
 
44
-
45
52
  def run_command(command = '')
46
53
  winrm = Chef::Knife::Winrm.new
47
54
  winrm.name_args = [ server_name, command ]
48
55
  winrm.config[:winrm_user] = locate_config_value(:winrm_user)
49
56
  winrm.config[:winrm_password] = locate_config_value(:winrm_password)
50
57
  winrm.config[:winrm_transport] = locate_config_value(:winrm_transport)
51
- winrm.config[:kerberos_keytab_file] = Chef::Config[:knife][:kerberos_keytab_file] if Chef::Config[:knife][:kerberos_keytab_file]
52
- winrm.config[:kerberos_realm] = Chef::Config[:knife][:kerberos_realm] if Chef::Config[:knife][:kerberos_realm]
53
- winrm.config[:kerberos_service] = Chef::Config[:knife][:kerberos_service] if Chef::Config[:knife][:kerberos_service]
54
- winrm.config[:ca_trust_file] = Chef::Config[:knife][:ca_trust_file] if Chef::Config[:knife][:ca_trust_file]
58
+ winrm.config[:winrm_ssl_verify_mode] = locate_config_value(:winrm_ssl_verify_mode)
59
+ winrm.config[:kerberos_keytab_file] = locate_config_value(:kerberos_keytab_file) if locate_config_value(:kerberos_keytab_file)
60
+ winrm.config[:kerberos_realm] = locate_config_value(:kerberos_realm) if locate_config_value(:kerberos_realm)
61
+ winrm.config[:kerberos_service] = locate_config_value(:kerberos_service) if locate_config_value(:kerberos_service)
62
+ winrm.config[:ca_trust_file] = locate_config_value(:ca_trust_file) if locate_config_value(:ca_trust_file)
55
63
  winrm.config[:manual] = true
56
64
  winrm.config[:winrm_port] = locate_config_value(:winrm_port)
57
65
  winrm.config[:suppress_auth_failure] = true
58
-
59
- #If you turn off the return flag, then winrm.run won't atually check and
66
+
67
+ #If you turn off the return flag, then winrm.run won't atually check and
60
68
  #return the error
61
- #codes. Otherwise, it ignores the return value of the server call.
69
+ #codes. Otherwise, it ignores the return value of the server call.
62
70
  winrm.config[:returns] = "0"
63
71
  winrm.run
64
72
  end
@@ -99,4 +107,3 @@ class Chef
99
107
  end
100
108
  end
101
109
  end
102
-
@@ -22,6 +22,7 @@ require 'chef/knife/core/bootstrap_context'
22
22
  require 'knife-windows/path_helper'
23
23
  # require 'chef/util/path_helper'
24
24
 
25
+
25
26
  class Chef
26
27
  class Knife
27
28
  module Core
@@ -34,27 +35,37 @@ class Chef
34
35
  class WindowsBootstrapContext < BootstrapContext
35
36
  PathHelper = ::Knife::Windows::PathHelper
36
37
 
37
- def initialize(config, run_list, chef_config)
38
+ attr_accessor :client_pem
39
+
40
+ def initialize(config, run_list, chef_config, secret=nil)
38
41
  @config = config
39
42
  @run_list = run_list
40
43
  @chef_config = chef_config
41
- super(config, run_list, chef_config)
44
+ @secret = secret
45
+ # Compatibility with Chef 12 and Chef 11 versions
46
+ begin
47
+ # Pass along the secret parameter for Chef 12
48
+ super(config, run_list, chef_config, secret)
49
+ rescue ArgumentError
50
+ # The Chef 11 base class only has parameters for initialize
51
+ super(config, run_list, chef_config)
52
+ end
42
53
  end
43
54
 
44
55
  def validation_key
45
- if super
46
- escape_and_echo(super)
56
+ if File.exist?(File.expand_path(@chef_config[:validation_key]))
57
+ IO.read(File.expand_path(@chef_config[:validation_key]))
47
58
  else
48
- raise 'Knife-Windows < 1.0 does not support validatorless bootstraps'
59
+ false
49
60
  end
50
61
  end
51
62
 
52
- def encrypted_data_bag_secret
53
- escape_and_echo(@config[:encrypted_data_bag_secret])
63
+ def secret
64
+ escape_and_echo(@config[:secret])
54
65
  end
55
66
 
56
- def trusted_certs
57
- @trusted_certs ||= trusted_certs_content
67
+ def trusted_certs_script
68
+ @trusted_certs_script ||= trusted_certs_content
58
69
  end
59
70
 
60
71
  def config_content
@@ -64,8 +75,6 @@ log_location STDOUT
64
75
 
65
76
  chef_server_url "#{@chef_config[:chef_server_url]}"
66
77
  validation_client_name "#{@chef_config[:validation_client_name]}"
67
- client_key "c:/chef/client.pem"
68
- validation_key "c:/chef/validation.pem"
69
78
 
70
79
  file_cache_path "c:/chef/cache"
71
80
  file_backup_path "c:/chef/backup"
@@ -119,12 +128,12 @@ CONFIG
119
128
  client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n}
120
129
  end
121
130
 
122
- if @config[:encrypted_data_bag_secret]
131
+ if @config[:secret]
123
132
  client_rb << %Q{encrypted_data_bag_secret "c:/chef/encrypted_data_bag_secret"\n}
124
133
  end
125
134
 
126
- unless trusted_certs.empty?
127
- client_rb << %Q{trusted_certs_dir "C:/chef/trusted_certs"\n}
135
+ unless trusted_certs_script.empty?
136
+ client_rb << %Q{trusted_certs_dir "c:/chef/trusted_certs"\n}
128
137
  end
129
138
 
130
139
  escape_and_echo(client_rb)
@@ -158,10 +167,30 @@ CONFIG
158
167
  end
159
168
 
160
169
  def win_wget
170
+ # I tried my best to figure out how to properly url decode and switch / to \
171
+ # but this is VBScript - so I don't really care that badly.
161
172
  win_wget = <<-WGET
162
173
  url = WScript.Arguments.Named("url")
163
174
  path = WScript.Arguments.Named("path")
164
175
  proxy = null
176
+ '* Vaguely attempt to handle file:// scheme urls by url unescaping and switching all
177
+ '* / into \. Also assume that file:/// is a local absolute path and that file://<foo>
178
+ '* is possibly a network file path.
179
+ If InStr(url, "file://") = 1 Then
180
+ url = Unescape(url)
181
+ If InStr(url, "file:///") = 1 Then
182
+ sourcePath = Mid(url, Len("file:///") + 1)
183
+ Else
184
+ sourcePath = Mid(url, Len("file:") + 1)
185
+ End If
186
+ sourcePath = Replace(sourcePath, "/", "\\")
187
+
188
+ Set objFSO = CreateObject("Scripting.FileSystemObject")
189
+ If objFSO.Fileexists(path) Then objFSO.DeleteFile path
190
+ objFSO.CopyFile sourcePath, path, true
191
+ Set objFSO = Nothing
192
+
193
+ Else
165
194
  Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP")
166
195
  Set wshShell = CreateObject( "WScript.Shell" )
167
196
  Set objUserVariables = wshShell.Environment("USER")
@@ -200,8 +229,9 @@ Set objFSO = Nothing
200
229
  objADOStream.SaveToFile path
201
230
  objADOStream.Close
202
231
  Set objADOStream = Nothing
203
- End if
232
+ End If
204
233
  Set objXMLHTTP = Nothing
234
+ End If
205
235
  WGET
206
236
  escape_and_echo(win_wget)
207
237
  end
@@ -235,6 +265,21 @@ WGET_PS
235
265
  local_download_path = "%TEMP%\\chef-client-latest.msi"
236
266
  end
237
267
 
268
+ def msi_url(machine_os=nil, machine_arch=nil, download_context=nil)
269
+ # The default msi path has a number of url query parameters - we attempt to substitute
270
+ # such parameters in as long as they are provided by the template.
271
+
272
+ if @config[:msi_url].nil? || @config[:msi_url].empty?
273
+ url = "https://www.chef.io/chef/download?p=windows"
274
+ url += "&pv=#{machine_os}" unless machine_os.nil?
275
+ url += "&m=#{machine_arch}" unless machine_arch.nil?
276
+ url += "&DownloadContext=#{download_context}" unless download_context.nil?
277
+ url += latest_current_windows_chef_version_query
278
+ else
279
+ @config[:msi_url]
280
+ end
281
+ end
282
+
238
283
  def first_boot
239
284
  first_boot_attributes_and_run_list = (@config[:first_boot_attributes] || {}).merge(:run_list => @run_list)
240
285
  escape_and_echo(first_boot_attributes_and_run_list.to_json)
@@ -250,9 +295,15 @@ WGET_PS
250
295
  private
251
296
 
252
297
  def install_command(executor_quote)
253
- "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
298
+ if @config[:install_as_service]
299
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote} ADDLOCAL=#{executor_quote}ChefClientFeature,ChefServiceFeature#{executor_quote}"
300
+ else
301
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
302
+ end
254
303
  end
255
304
 
305
+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
306
+ # This string should contain both the commands necessary to both create the files, as well as their content
256
307
  def trusted_certs_content
257
308
  content = ""
258
309
  if @chef_config[:trusted_certs_dir]
@@ -0,0 +1,155 @@
1
+ # Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014 Opscode, 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/knife'
19
+ require 'chef/knife/winrm_base'
20
+ require 'openssl'
21
+ require 'socket'
22
+
23
+ class Chef
24
+ class Knife
25
+ class WindowsCertGenerate < Knife
26
+
27
+ attr_accessor :thumbprint, :hostname
28
+
29
+ banner "knife windows cert generate FILE_PATH (options)"
30
+
31
+ option :hostname,
32
+ :short => "-H HOSTNAME",
33
+ :long => "--hostname HOSTNAME",
34
+ :description => "Use to specify the hostname for the listener.
35
+ For example, --hostname something.mydomain.com or *.mydomain.com.",
36
+ :required => true
37
+
38
+ option :output_file,
39
+ :short => "-o PATH",
40
+ :long => "--output-file PATH",
41
+ :description => "Specifies the file path at which to generate the 3 certificate files of type .pfx, .b64, and .pem. The default is './winrmcert'.",
42
+ :default => "winrmcert"
43
+
44
+ option :key_length,
45
+ :short => "-k LENGTH",
46
+ :long => "--key-length LENGTH",
47
+ :description => "Default is 2048",
48
+ :default => "2048"
49
+
50
+ option :cert_validity,
51
+ :short => "-cv MONTHS",
52
+ :long => "--cert-validity MONTHS",
53
+ :description => "Default is 24 months",
54
+ :default => "24"
55
+
56
+ option :cert_passphrase,
57
+ :short => "-cp PASSWORD",
58
+ :long => "--cert-passphrase PASSWORD",
59
+ :description => "Password for certificate."
60
+
61
+ def generate_keypair
62
+ OpenSSL::PKey::RSA.new(config[:key_length].to_i)
63
+ end
64
+
65
+ def prompt_for_passphrase
66
+ passphrase = ""
67
+ begin
68
+ print "Passphrases do not match. Try again.\n" unless passphrase.empty?
69
+ print "Enter certificate passphrase (empty for no passphrase):"
70
+ passphrase = STDIN.gets
71
+ return passphrase.strip if passphrase == "\n"
72
+ print "Enter same passphrase again:"
73
+ confirm_passphrase = STDIN.gets
74
+ end until passphrase == confirm_passphrase
75
+ passphrase.strip
76
+ end
77
+
78
+ def generate_certificate rsa_key
79
+ @hostname = config[:hostname] if config[:hostname]
80
+
81
+ #Create a self-signed X509 certificate from the rsa_key (unencrypted)
82
+ cert = OpenSSL::X509::Certificate.new
83
+ cert.version = 2
84
+ cert.serial = Random.rand(65534) + 1 # 2 digit byte range random number for better security aspect
85
+
86
+ cert.subject = OpenSSL::X509::Name.parse "/CN=#{@hostname}"
87
+ cert.issuer = cert.subject
88
+ cert.public_key = rsa_key.public_key
89
+ cert.not_before = Time.now
90
+ cert.not_after = cert.not_before + 2 * 365 * config[:cert_validity].to_i * 60 * 60 # 2 years validity
91
+ ef = OpenSSL::X509::ExtensionFactory.new
92
+ ef.subject_certificate = cert
93
+ ef.issuer_certificate = cert
94
+ cert.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false))
95
+ cert.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always",false))
96
+ cert.add_extension(ef.create_extension("extendedKeyUsage", "1.3.6.1.5.5.7.3.1", false))
97
+ cert.sign(rsa_key, OpenSSL::Digest::SHA1.new)
98
+ @thumbprint = OpenSSL::Digest::SHA1.new(cert.to_der)
99
+ cert
100
+ end
101
+
102
+ def write_certificate_to_file(cert, file_path, rsa_key)
103
+ File.open(file_path + ".pem", "wb") { |f| f.print cert.to_pem }
104
+ config[:cert_passphrase] = prompt_for_passphrase unless config[:cert_passphrase]
105
+ pfx = OpenSSL::PKCS12.create("#{config[:cert_passphrase]}", "winrmcert", rsa_key, cert)
106
+ File.open(file_path + ".pfx", "wb") { |f| f.print pfx.to_der }
107
+ File.open(file_path + ".b64", "wb") { |f| f.print Base64.strict_encode64(pfx.to_der) }
108
+ end
109
+
110
+ def certificates_already_exist?(file_path)
111
+ certs_exists = false
112
+ %w{pem pfx b64}.each do |extn|
113
+ if !Dir.glob("#{file_path}.*#{extn}").empty?
114
+ certs_exists = true
115
+ break
116
+ end
117
+ end
118
+
119
+ if certs_exists
120
+ begin
121
+ confirm("Do you really want to overwrite existing certificates")
122
+ rescue SystemExit # Need to handle this as confirming with N/n raises SystemExit exception
123
+ exit!
124
+ end
125
+ end
126
+ end
127
+
128
+ def run
129
+ STDOUT.sync = STDERR.sync = true
130
+
131
+ # takes user specified first cli value as a destination file path for generated cert.
132
+ file_path = @name_args.empty? ? config[:output_file].sub(/\.(\w+)$/,'') : @name_args.first
133
+
134
+ # check if certs already exists at given file path
135
+ certificates_already_exist? file_path
136
+
137
+ begin
138
+ filename = File.basename(file_path)
139
+ rsa_key = generate_keypair
140
+ cert = generate_certificate rsa_key
141
+ write_certificate_to_file cert, file_path, rsa_key
142
+ ui.info "Generated Certificates:"
143
+ ui.info "- #{filename}.pfx - PKCS12 format key pair. Contains public and private keys, can be used with an SSL server."
144
+ ui.info "- #{filename}.b64 - Base64 encoded PKCS12 key pair. Contains public and private keys, used by some cloud provider API's to configure SSL servers."
145
+ ui.info "- #{filename}.pem - Base64 encoded public certificate only. Required by the client to connect to the server."
146
+ ui.info "Certificate Thumbprint: #{@thumbprint.to_s.upcase}"
147
+ rescue => e
148
+ puts "ERROR: + #{e}"
149
+ end
150
+ end
151
+
152
+ end
153
+ end
154
+ end
155
+
@@ -0,0 +1,62 @@
1
+ # Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014 Opscode, 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/knife'
19
+ require 'chef/knife/winrm_base'
20
+
21
+ class Chef
22
+ class Knife
23
+ class WindowsCertInstall < Knife
24
+
25
+ banner "knife windows cert install CERT [CERT] (options)"
26
+
27
+ option :cert_passphrase,
28
+ :short => "-cp PASSWORD",
29
+ :long => "--cert-passphrase PASSWORD",
30
+ :description => "Password for certificate."
31
+
32
+ def get_cert_passphrase
33
+ print "Enter given certificate's passphrase (empty for no passphrase):"
34
+ passphrase = STDIN.gets
35
+ passphrase.strip
36
+ end
37
+
38
+ def run
39
+ STDOUT.sync = STDERR.sync = true
40
+ if @name_args.empty?
41
+ ui.error "Please specify the certificate path. e.g- 'knife windows cert install <path>"
42
+ exit 1
43
+ end
44
+ file_path = @name_args.first
45
+ config[:cert_passphrase] = get_cert_passphrase unless config[:cert_passphrase]
46
+
47
+ begin
48
+ ui.info "Adding certificate to the Windows Certificate Store..."
49
+ result = %x{powershell.exe -Command " '#{config[:cert_passphrase]}' | certutil -importPFX '#{file_path}' AT_KEYEXCHANGE"}
50
+ if $?.exitstatus == 0
51
+ ui.info "Certificate added to Certificate Store"
52
+ else
53
+ ui.info "Error adding the certificate. Use -VV option for details"
54
+ end
55
+ Chef::Log.debug "#{result}"
56
+ rescue => e
57
+ puts "ERROR: + #{e}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end